How to do simple C++ concept has_eq - that works with std::pair (is std::pair operator== broken for C++20)
Asked Answered
D

2

19

Compiler Explorer link

template <typename T>
concept HasEq = requires(T t) {
    { t == t } -> std::convertible_to<bool>;
};

struct X {};
static_assert(not HasEq<X>);
//bool a = pair<X, X>{} == pair<X, X>{};
static_assert(! HasEq<pair<X, X>>);  // fails! SIGH

I suppose it’s simple enough to define a concept for 'T has support for =='. And it’s simple enough to define a type 'X' which doesn't support the operator==. And the concept seems to work fine for that.

But it is confusing that pair<X,X> doesn't really support operator== (since it delegates to the X operator== that doesn’t exist).

And yet HasEq<pair<X, X>> returns the wrong answer (it says operator== is defined).

This appears to be a bug with the std C++ definitions of operator==(pair,pair), defining operator== unconditionally, instead of using 'enable_if' or 'requires' on the operator== definition. But I'm not really sure what I can do about that to make HasEq work properly (so it would start with understanding if this is really a defect in the std::pair operator== definition).

Dumb answered 19/6, 2023 at 22:27 Comment(7)
Ouch. The only option is to make this a trait (a template class), which you'd specialize for std::pair to do the right thing.Graz
std::pair<T1, T2>::operator== is indeed not SFINAE friendly.Euell
New operator <=> is fine BTW.Euell
I should have mentioned, my original code (I tried to rewrite to use concepts) was: - template <typename T> - constexpr inline bool has_eq_v = is_detected_v<Private_::has_eq_t, T>; - template <typename T, typename U> - constexpr inline bool has_eq_v<std::pair<T, U>> = has_eq_v<T> and has_eq_v<U>; - template <typename... Ts> - constexpr inline bool has_eq_v<std::tuple<Ts...>> = (has_eq_v<Ts> and ...);Dumb
But that doesnt appear to work (specialization) with concepts.Dumb
Note that static_assert( !HasEq<std::tuple<X, X>>); works.Missile
"This APPEARS to be a bug with the std c++ definitions": It is not a bug in the implementation of the standard library. The behavior you are seeing is what the standard's specification of std::pair::operator== requires. The change required to make it behave the way you want it to would be to modify the standard to make this SFINAE-friendly (e.g. via a LWG issue).Spoof
D
11

OK, I may have found an answer (thanks to hints in comments above!), but it makes me feel I need to bathe.

https://godbolt.org/z/3crzGdvP5

#include <concepts>
#include <utility>
using namespace std;

namespace PRIVATE_ {
template <typename T>
concept HasEqBasic = requires(T t) {
    { t == t } -> std::convertible_to<bool>;
};
template <typename T>
constexpr inline bool has_eq_v = HasEqBasic<T>;
template <typename T, typename U>
constexpr inline bool has_eq_v<std::pair<T, U>> = has_eq_v<T> and has_eq_v<U>;
template <typename... Ts>
constexpr inline bool has_eq_v<std::tuple<Ts...>> = (has_eq_v<Ts> and ...);
}  // namespace PRIVATE_

template <typename T>
concept HasEq = PRIVATE_::has_eq_v<T>;

struct X {};
static_assert(not HasEq<X>);
static_assert(!HasEq<pair<X, X>>);
Dumb answered 20/6, 2023 at 0:37 Comment(2)
You should also still check that HasEqBasic<std::pair<T, U>> is true in your specializations for std::pair, since someone could specialize std::pair itself and delete operator== from that specialization. Same goes for std::tuple. And they could also provide a working operator== even if T and U themselves don't have a working equality operator.Unhinge
But operator== isn't a MEMBER of pair<> so wouldn't be replaced with a specialization. Sure - they could specialize operator== to make it broken, but its already broken (in this situation by construction). But you ARE right they could provide a WORKING specialization. But I cannot see how to check for that. Using HasEqBasic as you suggest WONT WORK because if it did - I wouldn't have had the question in the first place. The problem is - IT RETURNS false positive true, even if the underlying code won't compile.Dumb
P
5

After some tinkering, I came up with this. Wiser heads will tell me what might be wrong with it:

template <typename T>
concept BasicHasEq = requires(T t) {
    { t == t } -> std::convertible_to <bool>;
};

template <typename T>
concept IsPair = requires (T t) {
    t.first;
    t.second;
};

template <typename T>
concept IsNonComparablePair = IsPair <T> &&
    (!BasicHasEq <decltype (std::declval <T> ().first)> ||
     !BasicHasEq <decltype (std::declval <T> ().second)>);

template <typename T>
concept IsContainer = requires (T t) {
    typename T::value_type;    
};    
    
template <typename T>
concept IsNonCopyableContainer = IsContainer <T> && !BasicHasEq <typename T::value_type>;

template <typename T>
concept HasEq = BasicHasEq <T> && !IsNonComparablePair <T> && !IsNonCopyableContainer <T>;

Live demo


Edited to also handle container types. More work probably still needed to handle all eventualities.

Prosperus answered 20/6, 2023 at 1:45 Comment(8)
THANK YOU. This ALSO appears to work and is a different approach than I can came up with. It SEEMS SLIGHTLY longer/more complicated than what I came up with, however.Dumb
Why are you using a duck-typing test for IsPair when the problem is singularly associated with std::pair and not any other types?Osyth
@BenVoigt Noted, thanks. No doubt the code can be simplified, this is a learning experience for me. But the problem isn't only limited to std::pair, there are other classes in the standard library that 'delegate' operator==.Prosperus
@Dumb No worries. Added a test for container types.Prosperus
@BenVoigt I noted the same problem with tuple(and my code works around that issue). Not sure which other cases are broken. Note the test for container you have may not be perfect, as for example, iterators also have value_type. Lots of things do.Dumb
@BenVoigt One thing I PERSONALLY believe works poorly in c++ concepts (which you are encountering as a special case) - is they really dont support types/subtypes, but just 'duck types'Dumb
@lewis: Hmmm, Eugene posted in a comment under the question that SFINAE worked correctly with tuple.Osyth
@BenVoigt - I get very inconsistent results when I try different compilers/libraries/versions with tuple. But for example, the MSVC definition is: _EXPORT_STD template <class... _Types1, class... _Types2> _NODISCARD constexpr bool operator==(const tuple<_Types1...>& _Left, const tuple<_Types2...>& _Right) { static_assert(sizeof...(_Types1) == sizeof...(_Types2), "cannot compare tuples of different sizes"); return _Left._Equals(_Right); } which doesn't appear to do any requires or SFINAE stuff to remove the definition. Still - it sometimes works there - so I must say - confused!Dumb

© 2022 - 2024 — McMap. All rights reserved.