如何实现一个简单的C++概念has_eq,使其与std::pair配合使用(C++20中的std::pair operator==是否存在问题)?

19

编译器探索器链接

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

我想定义一个概念“T具有==的支持”应该是足够简单的。而且定义一个不支持operator==的类型“X”也很简单。对于这个概念,似乎一切都运行良好。
但是让人困惑的是pair实际上并不支持operator==(因为它委托给了不存在的X operator==)。
然而,HasEq>返回了错误的答案(它说operator==已经定义)。
这似乎是std C++对operator==(pair,pair)的定义中的一个bug,它无条件地定义了operator==,而没有在operator==的定义中使用'enable_if'或'requires'。但我真的不确定我能做什么来使HasEq正常工作(所以首先要弄清楚这是否真的是std::pair operator==定义中的一个缺陷)。

2
std::pair<T1, T2>::operator== 确实不友好地支持 SFINAE。 - Jarod42
2
std::pair<T1, T2>::operator== 确实不太友好于 SFINAE。 - Jarod42
2
std::pair<T1, T2>::operator== 确实不太友好于 SFINAE。 - undefined
4
这似乎是std c++定义中的一个错误。实际上,这不是标准库实现中的错误。您所看到的行为正是std::pair::operator==在标准规范中要求的。如果要使其按您期望的方式工作,则需要修改标准,使其符合SFINAE(例如通过LWG问题)。 - user17732522
4
这似乎是标准C++定义中的一个错误。你所看到的行为正是std::pair::operator==在标准规范中要求的。要使其按照你期望的方式工作,需要修改标准以使其符合SFINAE(例如通过LWG问题)。 - user17732522
显示剩余11条评论
2个回答

11
好的,我可能找到了一个答案(多亏了上面评论中的提示!),但这让我感觉需要洗个澡。

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

在你对std::pair进行特化时,仍然需要检查HasEqBasic<std::pair<T, U>>是否为true,因为有人可能对std::pair本身进行特化并从该特化中删除operator==。对于std::tuple也是一样的情况。即使TU本身没有有效的相等运算符,他们也可能提供一个有效的operator== - G. Sliepen
在你对std::pair的特化中,你还应该检查HasEqBasic<std::pair<T, U>>是否为true,因为有人可能会特化std::pair本身并从该特化中删除operator==。对于std::tuple也是一样。即使TU本身没有有效的相等运算符,他们也可以提供一个有效的operator==。请确保进行相应的检查。 - G. Sliepen
1
但是operator==不是pair<>的成员,所以不能通过特化进行替换。当然,他们可以专门定义operator==来使其失效,但在这种情况下它已经由构造方式导致失效了。但你是对的,他们可以提供一个有效的特化。但我无法看到如何检查是否存在此特化。正如你建议的使用HasEqBasic是行不通的,因为如果可以,我一开始就不会提出这个问题。问题是,它返回了错误的正面true,即使底层代码无法编译。 - lewis
1
但是 operator== 不是 pair<> 的成员函数,所以不会被替换为特化版本。当然 - 他们可以专门化 operator== 来使其失效,但在这种情况下它已经是失效的(因为构造方式的问题)。但你是对的,他们可以提供一个可工作的特化版本。但我看不出如何检查。如你所建议的使用 HasEqBasic 是行不通的,因为如果行得通的话,我一开始就没有这个问题。问题是 - 它会返回错误的正面真值,即使底层代码无法编译。 - lewis

5
经过一番摸索,我想出了这个方案。更有经验的人会告诉我其中可能存在的问题。
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>;

实时演示


已编辑以处理容器类型。可能还需要更多工作来处理所有情况。


谢谢。这种方法看起来也能行,而且是一个与我想出的方法不同的方式。然而,它似乎稍微更长/更复杂一些。 - lewis
为什么在处理问题与 std::pair 独特相关而不是任何其他类型时,你要使用鸭子类型测试来判断 IsPair - Ben Voigt
1
@BenVoigt - 当我尝试使用不同的编译器/库/版本与元组进行操作时,结果非常不一致。但是举个例子,MSVC的定义是:_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), "无法比较不同大小的元组"); return _Left._Equals(_Right); } 这个定义似乎没有进行任何要求或SFINAE处理来移除定义。尽管如此,它在某些情况下仍然有效,所以我必须说,感到困惑! - lewis
1
@BenVoigt - 当我尝试使用不同的编译器/库/版本与元组进行比较时,结果非常不一致。但是举个例子,MSVC的定义如下: _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); } 这个定义似乎没有进行任何要求或SFINAE操作来删除定义。尽管如此,它在某些情况下仍然有效,所以我必须说,有点困惑! - lewis
1
@BenVoigt - 当我尝试不同的编译器/库/版本时,使用tuple时结果非常不一致。但以MSVC的定义为例,它是这样的:_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); } 看起来并没有进行任何要求或SFINAE的操作来移除定义。但有时它在这里确实有效 - 所以我必须说 - 感到困惑! - undefined
显示剩余16条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接