0

I have a problem while defining a template class containing T.

namespace
{
    template <typename T1, typename T2 = T1, typename Test = bool>
    struct is_comparable : std::integral_constant<bool, false> {};
    template <typename T1, typename T2>
    struct is_comparable<T1, T2, decltype(std::declval<const std::decay_t<T1>>() == std::declval<const std::decay_t<T2>>())> : std::integral_constant<bool, true> {};
}

template <class T>
class Wrapper
{
public:
    Wrapper() = default;
    void set(const T& val)
    {
        if constexpr (is_comparable<T>::value)
        {
            if (_val == val)
                return;
        }
        _val = val;
        onChanged();
    }
    void onChanged() { ... }
private:
    T _val;
};

The problem arises for std::map with a non-comparable value_type.

void foo()
{
    Wrapper<std::map<int, std::any>> mapWrapper;
    mapWrapper.set(std::map<int, std::any>()); // error occurs.
}

here's the cause.

static_assert(is_comparable<int>::value == true); // ok
static_assert(is_comparable<std::any>::value == false); // ok
static_assert(is_comparable<std::map<int, std::any>>::value == false); // error. I'm expecting false, but it's true.

// some code blocks in void Wrapper<T>::set(const T& val)
if constexpr (is_comparable<T>::value)
{
    // enter here when T == std::map<int, std::any>
    // and compile time error occurs.
    if (_val == val)
        return;
}

so I defined like this for avoiding the error, but is_comparable<T1, T2> is still used instead.

template <typename K1, typename V1, typename K2, typename V2>
struct is_comparable<std::map<K1, V1>, std::map<K2, V2>, std::enable_if_t<is_comparable<K1, K2>::value && is_comparable<V1, V2>::value, bool>> : std::integral_constant<bool, true> {};

is there any good resolution?

edit: full source codes and error messages is here.

#include <type_traits>
#include <map>
#include <any>

namespace
{

    template <typename T1, typename T2 = T1, typename Test = bool>
    struct is_comparable : std::integral_constant<bool, false> {};
    template <typename T1, typename T2>
    struct is_comparable<T1, T2, decltype(std::declval<const std::decay_t<T1>>() == std::declval<const std::decay_t<T2>>())> : std::integral_constant<bool, true> {};
    template <typename K1, typename V1, typename K2, typename V2>
    struct is_comparable<std::map<K1, V1>, std::map<K2, V2>, std::enable_if_t<is_comparable<K1, K2>::value&& is_comparable<V1, V2>::value, bool>> : std::integral_constant<bool, true> {};

} // unnamed namespace

template <class T>
class Wrapper
{
public:
    Wrapper() = default;
    void set(const T& val)
    {
        if constexpr (is_comparable<T>::value)
        {
            if (_val == val)
                return;
        }
        _val = val;
        onChanged();
    }
    void onChanged() { /* any expensive operations. */ }
private:
    T _val;
};

static_assert(is_comparable<int>::value == true);
static_assert(is_comparable<std::any>::value == false);
// static_assert(is_comparable<std::map<int, std::any>>::value == false); // error

void foo()
{
    Wrapper<std::map<int, std::any>> mapWrapper;
    mapWrapper.set(std::map<int, std::any>()); // error occurs.
}

int main(int /*argc*/, char** /*argv*/)
{
    foo();
    return 0;
}

messages

Build started... 1>------ Build started: Project: TemplateTest, Configuration: Debug x64 ------ 1>main.cpp 1>C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Tools\MSVC\14.35.32215\include\utility(368,56): error C2676: binary '==': 'const _Ty2' does not define this operator or a conversion to a type acceptable to the predefined operator 1>
with 1> [ 1> _Ty2=std::any 1> ] 1>C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Tools\MSVC\14.35.32215\include\utility(367,27): message : could be 'bool std::operator ==(const std::pair<_Ty1,_Ty2> &,const std::pair<_Ty1,_Ty2> &)' 1>C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Tools\MSVC\14.35.32215\include\utility(368,48): message : 'bool std::operator ==(const std::pair<_Ty1,_Ty2> &,const std::pair<_Ty1,_Ty2> &)': could not deduce template argument for 'const std::pair<_Ty1,_Ty2> &' from 'const _Ty2' 1> with 1>
[ 1> _Ty2=std::any 1> ] 1>C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Tools\MSVC\14.35.32215\include\utility(367,27): message : or 'bool std::operator ==(const std::pair<_Ty1,_Ty2> &,const std::pair<_Ty1,_Ty2> &)' 1>C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Tools\MSVC\14.35.32215\include\utility(368,48): message : 'bool std::operator ==(const std::pair<_Ty1,_Ty2> &,const std::pair<_Ty1,_Ty2> &)': could not deduce template argument for 'const std::pair<_Ty1,_Ty2> &' from 'const _Ty2' 1> with 1>
[ 1> _Ty2=std::any 1> ] 1>C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Tools\MSVC\14.35.32215\include\tuple(672,27): message : or 'bool std::operator ==(const std::tuple<_Types...> &,const std::tuple<_Types...> &)' 1>C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Tools\MSVC\14.35.32215\include\utility(368,48): message : 'bool std::operator ==(const std::tuple<_Types...> &,const std::tuple<_Types...> &)': could not deduce template argument for 'const std::tuple<_Types...> &' from 'const _Ty2' 1> with 1>
[ 1> _Ty2=std::any 1> ] 1>C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Tools\MSVC\14.35.32215\include\xutility(1441,5): message : or 'bool std::operator ==(const std::reverse_iterator<_BidIt> &,const std::reverse_iterator<_BidIt2> &) noexcept()' 1>C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Tools\MSVC\14.35.32215\include\utility(368,48): message : 'bool std::operator ==(const std::reverse_iterator<_BidIt> &,const std::reverse_iterator<_BidIt2> &) noexcept()': could not deduce template argument for 'const std::reverse_iterator<_BidIt> &' from 'const _Ty2' 1> with 1> [ 1>
_Ty2=std::any 1> ] 1>C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Tools\MSVC\14.35.32215\include\xutility(3969,5): message : or 'bool std::operator ==(const std::move_iterator<_Iter> &,const std::move_iterator<_Iter2> &) noexcept()' 1>C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Tools\MSVC\14.35.32215\include\utility(368,48): message : 'bool std::operator ==(const std::move_iterator<_Iter> &,const std::move_iterator<_Iter2> &) noexcept()': could not deduce template argument for 'const std::move_iterator<_Iter> &' from 'const _Ty2' 1> with 1> [ 1> _Ty2=std::any 1> ] 1>C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Tools\MSVC\14.35.32215\include\xmemory(894,30): message : or 'bool std::operator ==(const std::allocator<_Ty> &,const std::allocator<_Other> &) noexcept' 1>C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Tools\MSVC\14.35.32215\include\utility(368,48): message : 'bool std::operator ==(const std::allocator<_Ty> &,const std::allocator<_Other> &) noexcept': could not deduce template argument for 'const std::allocator<_Ty> &' from 'const _Ty2' 1>
with 1> [ 1> _Ty2=std::any 1> ] 1>C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Tools\MSVC\14.35.32215\include\map(399,17): message : or 'bool std::operator ==(const std::map<_Kty,_Ty,_Pr,_Alloc> &,const std::map<_Kty,_Ty,_Pr,_Alloc> &)' 1>C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Tools\MSVC\14.35.32215\include\utility(368,48): message : 'bool std::operator ==(const std::map<_Kty,_Ty,_Pr,_Alloc> &,const std::map<_Kty,_Ty,_Pr,_Alloc> &)': could not deduce template argument for 'const std::map<_Kty,_Ty,_Pr,_Alloc> &' from 'const _Ty2' 1> with 1> [ 1> _Ty2=std::any 1> ] 1>C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Tools\MSVC\14.35.32215\include\map(630,17): message : or 'bool std::operator ==(const std::multimap<_Kty,_Ty,_Pr,_Alloc> &,const std::multimap<_Kty,_Ty,_Pr,_Alloc> &)' 1>C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Tools\MSVC\14.35.32215\include\utility(368,48): message : 'bool std::operator ==(const std::multimap<_Kty,_Ty,_Pr,_Alloc> &,const std::multimap<_Kty,_Ty,_Pr,_Alloc> &)': could not deduce template argument for 'const std::multimap<_Kty,_Ty,_Pr,_Alloc> &' from 'const _Ty2' 1> with 1> [ 1> _Ty2=std::any 1> ] 1>C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Tools\MSVC\14.35.32215\include\xstddef(212): message : see reference to function template instantiation 'bool std::operator ==<const int,std::any>(const std::pair<const int,std::any> &,const std::pair<const int,std::any> &)' being compiled 1>C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Tools\MSVC\14.35.32215\include\xutility(4964): message : see reference to function template instantiation 'bool std::equal_to::operator ()<const std::pair<const int,std::any>&,const std::pair<const int,std::any>&>(_Ty1,_Ty2) noexcept(false) const' being compiled 1> with 1> [ 1>
_Ty1=const std::pair<const int,std::any> &, 1> _Ty2=const std::pair<const int,std::any> & 1> ] 1>C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Tools\MSVC\14.35.32215\include\xutility(4995): message : see reference to function template instantiation 'bool std::equal<_InIt1,_InIt2,std::equal_to>(const _InIt1,const _InIt1,const _InIt2,_Pr)' being compiled 1> with 1> [ 1> _InIt1=std::_Tree_unchecked_const_iterator<std::_Tree_val<std::_Tree_simple_types<std::pair<const int,std::any>>>,std::_Iterator_base0>, 1>
_InIt2=std::_Tree_unchecked_const_iterator<std::_Tree_val<std::_Tree_simple_types<std::pair<const int,std::any>>>,std::_Iterator_base0>, 1>
_Pr=std::equal_to 1> ] 1>C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Tools\MSVC\14.35.32215\include\map(399): message : see reference to function template instantiation 'bool std::equal<std::_Tree_unchecked_const_iterator<std::_Tree_val<std::_Tree_simple_types<std::pair<const int,std::any>>>,std::_Iterator_base0>,std::_Tree_unchecked_const_iterator<std::_Tree_val<std::_Tree_simple_types<std::pair<const int,std::any>>>,std::_Iterator_base0>>(const _InIt1,const _InIt1,const _InIt2)' being compiled 1> with 1> [ 1> _InIt1=std::_Tree_unchecked_const_iterator<std::_Tree_val<std::_Tree_simple_types<std::pair<const int,std::any>>>,std::_Iterator_base0>, 1>
_InIt2=std::_Tree_unchecked_const_iterator<std::_Tree_val<std::_Tree_simple_types<std::pair<const int,std::any>>>,std::_Iterator_base0> 1> ] 1>C:\Project\TemplateTest\TemplateTest\main.cpp(26,1): message : see reference to function template instantiation 'bool std::operator ==<int,std::any,std::less,std::allocator<std::pair<const int,std::any>>>(const std::map<int,std::any,std::less,std::allocator<std::pair<const int,std::any>>> &,const std::map<int,std::any,std::less,std::allocator<std::pair<const int,std::any>>> &)' being compiled 1>C:\Project\TemplateTest\TemplateTest\main.cpp(23,1): message : while compiling class template member function 'void Wrapperstd::map<int,std::any,std::less<int,std::allocator<std::pair<const int,std::any>>>>::set(const T &)' 1> with 1> [ 1>
T=std::map<int,std::any,std::less,std::allocator<std::pair<const int,std::any>>> 1> ] 1>C:\Project\TemplateTest\TemplateTest\main.cpp(44,19): message : see reference to function template instantiation 'void Wrapperstd::map<int,std::any,std::less<int,std::allocator<std::pair<const int,std::any>>>>::set(const T &)' being compiled 1> with 1>
[ 1>
T=std::map<int,std::any,std::less,std::allocator<std::pair<const int,std::any>>> 1> ] 1>C:\Project\TemplateTest\TemplateTest\main.cpp(43,38): message : see reference to class template instantiation 'Wrapperstd::map<int,std::any,std::less<int,std::allocator<std::pair<const int,std::any>>>>' being compiled 1>Done building project "TemplateTest.vcxproj" -- FAILED. ========== Build: 0 succeeded, 1 failed, 0 up-to-date, 0 skipped ========== ========== Build started at 5:43 PM and took 00.914 seconds ==========

(tried in msvc++ 14.35 / C++17)

Undefined
  • 3
  • 4
  • a [mcve] will make it much easier for others to look at your code and what it does and what the error is. You should also include the compiler error message in the question – 463035818_is_not_an_ai May 04 '23 at 07:56
  • @463035818_is_not_a_number ok. I added source codes and compiler error messages. – Undefined May 04 '23 at 08:48
  • `decltype(std::declval>() == std::declval>())` is `bool`. It's not clear what type you expected it to be. – molbdnilo May 04 '23 at 09:04
  • 2
    @Undefined I think you can simplify your question. All you ask is "How to implement is_comparable trait that will correctly check `std::map" and show that it's not comparable. No need for the wrapper class. – vvv444 May 04 '23 at 09:10
  • @molbdnilo they expect it to be a substitution failure – Caleth May 04 '23 at 09:25
  • @vvv444 I agree that the Wrapper class content looks like noise. But since there is answer focused on that class, I won't delete them. I'll edit the title though. – Undefined May 04 '23 at 11:47

2 Answers2

2

The problem with your code is that your is_comparable<> specialization for std::map is conditionally enabled for compilation only when the type is comparable. For non-comparable types, which is the case with std::map<int, std::any>, it falls back to the more generic specialization which evaluates to true. Here is how you can fix that:

template<class ...> using void_t = void;

template<typename L, typename R = L, class = void>
struct is_comparable : std::false_type {};

template<typename L, typename R>
using comparability = decltype(std::declval<L>() == std::declval<R>());

template<typename L, typename R>
struct is_comparable<L, R, void_t<comparability<L, R>>> : std::true_type {};

// std::map<> specialization
template<typename K1, typename V1, typename K2, typename V2>
struct is_comparable<std::map<K1, V1>, std::map<K2, V2>>
    : std::integral_constant<bool,
        is_comparable<K1, K2>::value &&
        is_comparable<V1, V2>::value
    > {};


static_assert(is_comparable<int>::value == true);
static_assert(is_comparable<std::any>::value == false);
static_assert(is_comparable<std::map<int, float>>::value == true);
static_assert(is_comparable<std::map<int, std::any>>::value == false);

Godbolt: https://godbolt.org/z/77bs6re7e

On a more general note, the unfortunate answer is that it seems impossible to implement such is_comparable<> trait in C++17 without defining specializations for each specific type (like you did with std::map).

The reason for this is because the decltype(std::declval<L>() == std::declval<R>()) trick shown here only checks that the declaration is well formed but doesn't check that the comparison operator is actually well defined.

Even with C++20 std::equality_comparable<> concept we bump into the same problem. This answer explains that it would work if the C++ standard library had defined the right constraints on the container comparison operator, which it currently doesn't.

vvv444
  • 2,764
  • 1
  • 14
  • 25
  • `only checks that the declaration is well formed but doesn't check that the comparison operator is actually well defined.` it shows the point of my problem. – Undefined May 04 '23 at 11:54
  • @Undefined You actually greatly suggested the right way to solve this exact problem by explicit specialization, just needed a little bit more to make it work :-) – vvv444 May 04 '23 at 11:59
0

When the if constexpr succeeds, the next statement is an equality comparison that doesn't exist for std::map. The concept_map keyword which was dropped from concept proposal, might come in handy in such designs. Nevertheless, a closer look at STL reveals that a we can use a comparator as template parameter:

template <typename T, typename C>
class Wrapper
{
public:
    void set(const T& val)
    {
        if (C comp;comp(val, this->val_))
            return;
        //...                

Then you can define a custom comparison:

#include <concepts>
struct my_eq{
     constexpr bool operator()(auto const& lhs, auto const& rhs)
         requires std::equality_comparable_with<decltype(lhs), decltype(rhs)>
    { return rhs == lhs; };

    template<instance_of<std::map> L, instance_of<std::map> R>
         //requires std::equality_comparable_with<typenameR::mapped_type, typename L::mapped_type>
    constexpr bool operator()(L const& lhs, R const& rhs)
    { /*TODO: define map comparison*/ };
};

But before that, I need to define concept instance_of:

template<typename type, template<typename...> typename generic> struct instance_of_traits;
//TODO: use `std::same_as` to define the above

template<typename type, template<typename...> typename generic>
concept instance_of = bool{instance_of_traits<type, generic>::value};

Finally you can use your Wrapper:

Wrapper<std::map<int, std::any>, my_eq> mapWrapper;

However comparability of std::any should also be carefully handled too. Element-wise comparison of containers may negatively impact runtime, if used frequently. I guess you might decide another strategy after all.

Red.Wave
  • 2,790
  • 11
  • 17
  • This answer seems to be saying I'm stuck in an XY problem. However, it still requires explicit specialization for each type. Nevertheless, thanks for useful answer. – Undefined May 04 '23 at 11:50
  • Maybe it's my problem. I often turn the axis (XY) to simplify the problem. I only go for complications when a change of perspectives doesn't help anymore. But if you separate the comparison logic (as STL does), up with more generic code you'll end. – Red.Wave May 05 '23 at 13:39