Undefined
Undefined

Reputation: 13

How to implement is_comparable trait that will correctly check std::map with non-comparable value_type?

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)

Upvotes: 1

Views: 255

Answers (2)

vvv444
vvv444

Reputation: 3122

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.

Upvotes: 2

Red.Wave
Red.Wave

Reputation: 4249

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.

Upvotes: 0

Related Questions