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).

lewis
  • 1,116
  • 1
  • 12
  • 28
  • 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. – HolyBlackCat Jun 19 '23 at 22:35
  • 2
    `std::pair::operator==` is indeed not SFINAE friendly. – Jarod42 Jun 19 '23 at 22:40
  • New `operator <=>` is fine BTW. – Jarod42 Jun 19 '23 at 22:47
  • I should have mentioned, my original code (I tried to rewrite to use concepts) was: - template - constexpr inline bool has_eq_v = is_detected_v; - template - constexpr inline bool has_eq_v> = has_eq_v and has_eq_v; - template - constexpr inline bool has_eq_v> = (has_eq_v and ...); – lewis Jun 19 '23 at 22:51
  • But that doesnt appear to work (specialization) with concepts. – lewis Jun 19 '23 at 22:52
  • 1
    Note that `static_assert( !HasEq>);` works. – Eugene Jun 19 '23 at 23:42
  • 4
    "_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](https://cplusplus.github.io/LWG/)). – user17732522 Jun 19 '23 at 23:50

2 Answers2

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>>);
Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
lewis
  • 1,116
  • 1
  • 12
  • 28
  • You should also still check that `HasEqBasic>` 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. – G. Sliepen Jun 20 '23 at 11:53
  • 1
    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. – lewis Jun 20 '23 at 15:23
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.

Paul Sanders
  • 24,133
  • 4
  • 26
  • 48
  • 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. – lewis Jun 20 '23 at 15:29
  • Why are you using a duck-typing test for `IsPair` when the problem is singularly associated with `std::pair` and not any other types? – Ben Voigt Jun 20 '23 at 16:17
  • @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==`. – Paul Sanders Jun 20 '23 at 16:20
  • @lewis No worries. Added a test for container types. – Paul Sanders Jun 20 '23 at 16:20
  • @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. – lewis Jun 20 '23 at 17:33
  • @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' – lewis Jun 20 '23 at 17:34
  • @lewis: Hmmm, Eugene posted in a comment under the question that SFINAE worked correctly with tuple. – Ben Voigt Jun 20 '23 at 18:14
  • 1
    @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 _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! – lewis Jun 21 '23 at 17:53