Hum*_*ler 50 c++ language-lawyer c++-concepts c++20
使用 C++20'sconcept我注意到这std::unique_ptr似乎无法满足这个std::equality_comparable_with<std::nullptr_t,...>概念。从std::unique_ptr的定义来看,它应该在 C++20 中实现以下内容:
template<class T1, class D1, class T2, class D2>
bool operator==(const unique_ptr<T1, D1>& x, const unique_ptr<T2, D2>& y);
template <class T, class D>
bool operator==(const unique_ptr<T, D>& x, std::nullptr_t) noexcept;
Run Code Online (Sandbox Code Playgroud)
这个要求应该实现对称比较nullptr——根据我的理解,这足以满足equality_comparable_with.
奇怪的是,这个问题似乎在所有主要编译器上都是一致的。以下代码被 Clang、GCC 和 MSVC 拒绝:
// fails on all three compilers
static_assert(std::equality_comparable_with<std::unique_ptr<int>,std::nullptr_t>);
Run Code Online (Sandbox Code Playgroud)
然而,同样的断言与std::shared_ptr被接受:
// succeeds on all three compilers
static_assert(std::equality_comparable_with<std::shared_ptr<int>,std::nullptr_t>);
Run Code Online (Sandbox Code Playgroud)
除非我误解了什么,否则这似乎是一个错误。我的问题是这是否是三个编译器实现中的巧合错误,还是 C++20 标准中的缺陷?
注意:如果这恰好是一个缺陷,我会标记这个语言律师。
Jus*_*tin 60
TL; DR:std::equality_comparable_with<T, U>需要两个T和U可转换到的所述公共参考T和U。对于std::unique_ptr<T>and的情况std::nullptr_t,这要求它std::unique_ptr<T>是可复制构造的,而事实并非如此。
系好安全带。这真是一段旅程。把我当成书呆子狙击手吧。
std::equality_comparable_with 要求:
Run Code Online (Sandbox Code Playgroud)template <class T, class U> concept equality_comparable_with = std::equality_comparable<T> && std::equality_comparable<U> && std::common_reference_with< const std::remove_reference_t<T>&, const std::remove_reference_t<U>&> && std::equality_comparable< std::common_reference_t< const std::remove_reference_t<T>&, const std::remove_reference_t<U>&>> && __WeaklyEqualityComparableWith<T, U>;
那是一口。将概念分解成各个部分,std::equality_comparable_with<std::unique_ptr<int>, std::nullptr_t>失败了std::common_reference_with<const std::unique_ptr<int>&, const std::nullptr_t&>:
Run Code Online (Sandbox Code Playgroud)template <class T, class U> concept equality_comparable_with = std::equality_comparable<T> && std::equality_comparable<U> && std::common_reference_with< const std::remove_reference_t<T>&, const std::remove_reference_t<U>&> && std::equality_comparable< std::common_reference_t< const std::remove_reference_t<T>&, const std::remove_reference_t<U>&>> && __WeaklyEqualityComparableWith<T, U>;
(为了易读性而编辑)编译器资源管理器链接。
std::common_reference_with 要求:
Run Code Online (Sandbox Code Playgroud)template < class T, class U > concept common_reference_with = std::same_as<std::common_reference_t<T, U>, std::common_reference_t<U, T>> && std::convertible_to<T, std::common_reference_t<T, U>> && std::convertible_to<U, std::common_reference_t<T, U>>;
std::common_reference_t<const std::unique_ptr<int>&, const std::nullptr_t&>是std::unique_ptr<int>(请参阅编译器资源管理器链接)。
将这些放在一起,有一个传递要求 that std::convertible_to<const std::unique_ptr<int>&, std::unique_ptr<int>>,这相当于要求 thatstd::unique_ptr<int>是可复制构造的。
std::common_reference_t不是参考?为什么是std::common_reference_t<const std::unique_ptr<T>&, const std::nullptr_t&> = std::unique_ptr<T>而不是const std::unique_ptr<T>&?std::common_reference_t两种类型(sizeof...(T)是两个)的文档说:
- 如果
T1和T2都引用类型,和简单的公共参考类型S的T1和T2(如下面所定义)存在,则该成员类型类型名S;- 否则,如果
std::basic_common_reference<std::remove_cvref_t<T1>, std::remove_cvref_t<T2>, T1Q, T2Q>::type存在,其中TiQ是一个一元别名模板,使得TiQ<U>是U在添加Ti的CV-和参考限定符,则成员类型类型名该类型;- 否则,如果
decltype(false? val<T1>() : val<T2>())val 是函数模板template<class T> T val();,则如果 是有效类型,则成员类型类型命名该类型;- 否则,如果
std::common_type_t<T1, T2>是有效类型,则成员类型类型命名该类型;- 否则,没有成员类型。
const std::unique_ptr<T>&并且const std::nullptr_t&没有简单的公共引用类型,因为引用不能立即转换为公共基类型(即false ? crefUPtr : crefNullptrT格式错误)。没有std::basic_common_reference专业化std::unique_ptr<T>。第三个选项也失败了,但我们触发了std::common_type_t<const std::unique_ptr<T>&, const std::nullptr_t&>.
对于std::common_type, std::common_type<const std::unique_ptr<T>&, const std::nullptr_t&> = std::common_type<std::unique_ptr<T>, std::nullptr_t>, 因为:
如果应用于
std::decay至少一个T1并且T2产生不同的类型,则成员类型命名与 相同的类型(std::common_type<std::decay<T1>::type, std::decay<T2>::type>::type如果存在);如果不是,则没有成员类型。
std::common_type<std::unique_ptr<T>, std::nullptr_t>事实上存在;它是std::unique_ptr<T>。这就是引用被剥离的原因。
这已经变成了P2404,其中提出修改std::equality_comparable_with,std::totally_ordered_with以及std::three_way_comparable_with只支持移动类型。
在`equality_comparable_with` 是否需要要求`common_reference`?,TC(最初来自n3351第 15-16 页)对公共参考要求给出的理由equality_comparable_with是:
[W]两个不同类型的值相等意味着什么?该设计表示跨类型相等是通过将它们映射到公共(引用)类型来定义的(需要这种转换来保留值)。
仅仅要求==可能对概念天真地预期的操作是行不通的,因为:
[I]t 允许拥有
t == u和t2 == u但是t != t2
因此,通用参考要求是为了数学上的健全性,同时允许以下可能的实现:
using common_ref_t = std::common_reference_t<const Lhs&, const Rhs&>;
common_ref_t lhs = lhs_;
common_ref_t rhs = rhs_;
return lhs == rhs;
Run Code Online (Sandbox Code Playgroud)
使用 n3351 支持的 C++0X 概念,如果没有异构operator==(T, U). 对于 C++20 的概念,我们需要一个异构operator==(T, U)的存在,所以永远不会使用这个实现。
注意,n3351 表示这种异构等式已经是等式的扩展,它只是在单一类型内进行了严格的数学定义。实际上,当我们编写异构相等操作时,我们假装这两种类型共享一个共同的超类型,而操作发生在该共同类型内部。
也许公共参考要求std::equality_comparable太严格了。重要的是,数学要求只是存在一个共同的超类型,其中这个提升operator==是一个平等,但共同的参考要求要求更严格,另外要求:
std::common_reference_t.放松第一点基本上只是提供一个明确的定制点std::equality_comparable_with,您可以在其中明确选择一对类型来满足概念。对于第二点,在数学上,“参考”是没有意义的。因此,第二点也可以放宽,以允许公共超类型可以从两种类型隐式转换。
这是很难做到的。重要的是,我们实际上只关心公共超类型是否存在,但我们实际上从不需要在代码中使用它。因此,我们无需担心效率,甚至在编写通用超类型转换时是否无法实现。
这可以通过更改 的std::common_reference_with部分来实现equality_comparable_with:
<source>:6:20: note: constraints not satisfied
In file included from <source>:1:
/…/concepts:72:13: required for the satisfaction of
'convertible_to<_Tp, typename std::common_reference<_Tp1, _Tp2>::type>'
[with _Tp = const std::unique_ptr<int, std::default_delete<int> >&; _Tp2 = const std::nullptr_t&; _Tp1 = const std::unique_ptr<int, std::default_delete<int> >&]
/…/concepts:72:30: note: the expression 'is_convertible_v<_From, _To>
[with _From = const std::unique_ptr<int, std::default_delete<int> >&; _To = std::unique_ptr<int, std::default_delete<int> >]' evaluated to 'false'
72 | concept convertible_to = is_convertible_v<_From, _To>
| ^~~~~~~~~~~~~~~~~~~~~~~~~~~~
Run Code Online (Sandbox Code Playgroud)
特别是,更改正在更改common_reference_with为这种假设__CommonSupertypeWith,其中__CommonSupertypeWith不同之处在于允许std::common_reference_t<T, U>生成Tor的引用剥离版本,U并且还尝试同时尝试C(T&&)和C(const T&)创建公共引用。有关更多详细信息,请参阅P2404。
std::equality_comparable_with在将其合并到标准中之前,我该如何解决?对于标准库中的所有使用std::equality_comparable_with(或任何其他*_with概念),有一个谓词重载,您可以将函数传递给它。这意味着您可以只传递std::equal_to()给谓词重载并获得所需的行为(不是 std::ranges::equal_to受约束的,而是不受约束的std::equal_to)。
然而,这并不意味着不修复是一个好主意std::equality_comparable_with。
std::equality_comparable_with吗?通用参考需求使用std::common_reference_t,其定制点为std::basic_common_reference,目的是:
类模板
basic_common_reference是一个自定义点,允许用户影响common_reference用户定义类型(通常是代理引用)的结果。
这是一个可怕的黑客,但是如果我们编写一个代理引用来支持我们想要比较的两种类型,我们可以专门std::basic_common_reference针对我们的类型,使我们的类型能够满足std::equality_comparable_with. 另请参阅如何告诉编译器 MyCustomType 是等式_comparable_with SomeOtherType?. 如果您选择这样做,请注意;std::common_reference_t不仅被std::equality_comparable_with或其他comparison_relation_with概念使用,您还有可能在未来导致级联问题。最好确保公共引用实际上是公共引用,例如:
template < class T, class U >
concept common_reference_with =
std::same_as<std::common_reference_t<T, U>, std::common_reference_t<U, T>> &&
std::convertible_to<T, std::common_reference_t<T, U>> &&
std::convertible_to<U, std::common_reference_t<T, U>>;
Run Code Online (Sandbox Code Playgroud)
custom_vector_ref<T>对于custom_vector<T>和之间custom_vector_ref<T>,甚至可能是custom_vector<T>和之间的公共引用,可能是一个不错的选择std::array<T, N>。小心踩踏。
std::equality_comparable_with?你不能。专门std::basic_common_reference针对您不拥有的std::类型(类型或某些第三方库)充其量是不好的做法,最坏的情况是未定义的行为。最安全的选择是使用您拥有的可以比较的代理类型,或者编写您自己的扩展,std::equality_comparable_with该扩展具有用于自定义相等拼写的显式自定义点。
在数学上,相等是一种等价关系。然而,等价关系是在单个集合上定义的。那么我们如何定义两个集合A和之间的等价关系B呢?简单地说,我们改为定义 上的等价关系C = A?B。也就是说,我们取Aand 的一个公共超类型,并B在这个超类型上定义等价关系。
这意味着我们的关系c1 == c2必须无论身在何处定义c1和c2从何而来,所以我们必须有a1 == a2,a == b和b1 == b2(其中ai是A与bi来自B)。转换为 C++,这意味着所有operator==(A, A), operator==(A, B), operator==(B, B), 和operator==(C, C)必须是相同等式的一部分。
这就是为什么iterator/ sentinels 不满足的原因std::equality_comparable_with:虽然operator==(iterator, sentinel)实际上可能是某个等价关系的一部分,但它不是与operator==(iterator, iterator)(否则迭代器相等性只会回答“最后是两个迭代器还是两个迭代器?不是最后?”)。
实际上很容易写出一个operator==实际上不是相等的,因为你必须记住异类相等不是operator==(A, B)你写的单一,而是四个不同的operator==s,它们必须全部是内聚的。
operator==s;我们为什么不能只是operator==(C, C)和operator==(A, B)优化的目的呢?这是一个有效的模型,我们可以这样做。然而,C++ 并不是柏拉图式的现实。尽管概念尽最大努力只接受真正满足语义要求的类型,但它实际上无法实现这一目标。因此,如果我们只检查operator==(A, B)和operator==(C, C),我们运行的风险,operator==(A, A)并operator==(B, B)做不同的事情。此外,如果我们能有operator==(C, C),那么这意味着它是微不足道的写operator==(A, A)和operator==(B, B)基于我们有operator==(C, C)。也就是说,要求operator==(A, A)和的危害operator==(B, B)是相当低的,作为回报,我们得到了更高的信心,即我们实际上已经平等了。
但是,在某些情况下,这会遇到困难。见P2405。
operator==(A, B)是一个实际的平等吗?我永远不会实际使用operator==(A, A)or operator==(B, B);我只关心能够进行交叉类型比较。实际上,我们需要operator==(A, B)一个实际相等的模型可能会起作用。在这个模型下,我们会有std::equality_comparable_with<iterator, sentinel>,但是在所有已知的上下文中这究竟意味着什么可以敲定。然而,这不是标准的方向是有原因的,在了解是否或如何改变它之前,他们必须首先了解为什么选择标准的模型。