TL;DR: std::equality_comparable_with<T, U>
要求两者T
and U
可转换为共同参考T
and U
。对于以下情况std::unique_ptr<T>
and std::nullptr_t
,这需要std::unique_ptr<T>
是可复制构造的,但事实并非如此。
系好安全带。这真是一段旅程。考虑我书呆子狙击手 https://xkcd.com/356/.
为什么我们不满足这个概念呢?
std::equality_comparable_with https://en.cppreference.com/w/cpp/concepts/equality_comparable要求:
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&>
:
<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>
| ^~~~~~~~~~~~~~~~~~~~~~~~~~~~
(为了易读性进行了编辑)编译器资源管理器链接 https://gcc.godbolt.org/z/5jcKbEaKM.
std::common_reference_with https://en.cppreference.com/w/cpp/concepts/common_reference_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>>;
std::common_reference_t<const std::unique_ptr<int>&, const std::nullptr_t&>
is std::unique_ptr<int>
(see 编译器资源管理器链接 https://gcc.godbolt.org/z/f797eceeK).
把它们放在一起,有一个传递性要求std::convertible_to<const std::unique_ptr<int>&, std::unique_ptr<int>>
,这相当于要求std::unique_ptr<int>
是可复制构造的。
为什么是std::common_reference_t
不是参考?
Why is 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 https://en.cppreference.com/w/cpp/types/common_reference对于两种类型(sizeof...(T)
是二)说:
- If
T1
and T2
都是引用类型,并且简单通用引用类型 S
of T1
and T2
(如下定义)存在,那么
成员类型类型名称S
;
- 否则,如果
std::basic_common_reference<std::remove_cvref_t<T1>, std::remove_cvref_t<T2>, T1Q, T2Q>::type
存在,在哪里TiQ
是一元
别名模板使得TiQ<U>
is U
添加Ti
的简历- 和
引用限定符,然后是该类型的成员类型类型名称;
- 否则,如果
decltype(false? val<T1>() : val<T2>())
,其中 val 是函数模板template<class T> T val();
, 是有效类型,那么
成员类型 类型名称 该类型;
- 否则,如果
std::common_type_t<T1, T2>
是有效类型,则成员类型类型命名该类型;
- 否则,没有成员类型。
const std::unique_ptr<T>&
and 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&>
.
For std::common_type https://en.cppreference.com/w/cpp/types/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
and 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 https://wg21.link/p2404,建议更改为std::equality_comparable_with
, std::totally_ordered_with
, and std::three_way_comparable_with
支持仅移动类型。
为什么我们有这些共同参考的要求?
In `equality_comparable_with` 是否需要需要 `common_reference` ? https://stackoverflow.com/q/61177302/1896169, the T.C. 给出的理由 https://stackoverflow.com/a/61181916/1896169(最初源自n3351 https://wg21.link/n3351第 15-16 页)了解通用参考要求equality_comparable_with
is:
[W]两个不同类型的值相等是什么意思?该设计表示,跨类型相等性是通过将它们映射到公共(引用)类型来定义的(需要此转换来保留值)。
只需要==
天真地期望该概念的操作不起作用,因为:
[I]不允许有t == u
and t2 == u
but 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;
使用 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
:
template <class T, class U>
concept equality_comparable_with =
__WeaklyEqualityComparableWith<T, U> &&
std::equality_comparable<T> &&
std::equality_comparable<U> &&
std::equality_comparable<
std::common_reference_t<
const std::remove_reference_t<T>&,
const std::remove_reference_t<U>&>> &&
__CommonSupertypeWith<T, U>;
template <class T, class U>
concept __CommonSupertypeWith =
std::same_as<
std::common_reference_t<
const std::remove_cvref_t<T>&,
const std::remove_cvref_t<U>&>,
std::common_reference_t<
const std::remove_cvref_t<U>&,
const std::remove_cvref_t<T>&>> &&
(std::convertible_to<const std::remove_cvref_t<T>&,
std::common_reference_t<
const std::remove_cvref_t<T>&,
const std::remove_cvref_t<U>&>> ||
std::convertible_to<std::remove_cvref_t<T>&&,
std::common_reference_t<
const std::remove_cvref_t<T>&,
const std::remove_cvref_t<U>&>>) &&
(std::convertible_to<const std::remove_cvref_t<U>&,
std::common_reference_t<
const std::remove_cvref_t<T>&,
const std::remove_cvref_t<U>&>> ||
std::convertible_to<std::remove_cvref_t<U>&&,
std::common_reference_t<
const std::remove_cvref_t<T>&,
const std::remove_cvref_t<U>&>>);
特别是变化正在发生变化common_reference_with
对于这个假设的__CommonSupertypeWith
where __CommonSupertypeWith
不同之处在于允许std::common_reference_t<T, U>
生成参考剥离版本T
or U
并同时尝试C(T&&)
and C(const T&)
创建公共参考。有关更多详细信息,请参阅P2404 https://wg21.link/p2404.
我该如何解决std::equality_comparable_with
在将其合并到标准之前?
更改您使用的重载
对于所有用途std::equality_comparable_with
(或任何其他*_with
标准库中的概念),有一个有用的谓词重载,您可以将函数传递给它。这意味着你可以通过std::equal_to()
到谓词重载并获得所需的行为(not std::ranges::equal_to
,这是有约束的,但无约束的std::equal_to
).
这并不意味着不修复是个好主意std::equality_comparable_with
, 然而。
我可以扩展自己的类型来满足std::equality_comparable_with
?
共同参考要求使用std::common_reference_t
,其定制点为std::basic_common_reference https://en.cppreference.com/w/cpp/types/common_reference, 为了...的目的:
类模板basic_common_reference
是一个定制点,允许用户影响结果common_reference
对于用户定义的类型(通常是代理引用)。
这是一个可怕的黑客行为,但是如果我们编写一个代理引用来支持我们想要比较的两种类型,我们就可以专门化std::basic_common_reference
适合我们的类型,使我们的类型能够满足std::equality_comparable_with
。也可以看看我如何告诉编译器 MyCustomType 与 SomeOtherType 是 equal_comparable_with SomeOtherType ? https://stackoverflow.com/q/66944119/1896169。如果您选择这样做,请小心;std::common_reference_t
不仅被使用std::equality_comparable_with
或其他comparison_relation_with
概念,您可能会面临引发一系列问题的风险。最好确保公共引用实际上是公共引用,例如:
template <typename T>
class custom_vector { ... };
template <typename T>
class custom_vector_ref { ... };
custom_vector_ref<T>
可能是一个很好的选择,可以作为之间的共同参考custom_vector<T>
and custom_vector_ref<T>
,或者甚至可能在之间custom_vector<T>
and std::array<T, N>
。小心行事。
如何扩展我无法控制的类型std::equality_comparable_with
?
你不能。专精std::basic_common_reference
对于您不拥有的类型(或者std::
类型或某些第三方库)往好了说是不好的做法,往坏了说是未定义的行为。最安全的选择是使用您拥有的代理类型,您可以通过它进行比较,或者编写您自己的扩展std::equality_comparable_with
它为您的自定义相等拼写有一个明确的自定义点。
好的,我知道这些要求的想法是数学健全性,但是这些要求如何实现数学健全性,以及为什么它如此重要?
从数学上讲,平等是一种等价关系。然而,等价关系是在单个集合上定义的。那么我们如何定义两个集合之间的等价关系A
and B
?简而言之,我们定义了等价关系C = A∪B
。也就是说,我们采用一个共同的超类型A
and B
并定义该超类型的等价关系。
这意味着我们的关系c1 == c2
无论在哪里都必须定义c1
and c2
来自,所以我们必须有a1 == a2
, a == b
, and b1 == b2
(where ai
来自A
and bi
来自B
)。转换为 C++,这意味着所有operator==(A, A)
, operator==(A, B)
, operator==(B, B)
, and operator==(C, C)
必须是同一平等的一部分。
这就是为什么iterator
/sentinel
不满足std::equality_comparable_with
: while operator==(iterator, sentinel)
实际上可能是某种等价关系的一部分,但它不是同一个等价关系的一部分operator==(iterator, iterator)
(否则迭代器相等只会回答“两个迭代器都在末尾还是两个迭代器都不在末尾?”的问题)。
实际上很容易写一个operator==
这实际上并不是平等,因为你必须记住,异构平等并不是单一的平等。operator==(A, B)
你正在写,但却是四个不同的operator==
这一切都必须具有凝聚力。
等一下,为什么我们需要全部四个operator==
s;为什么我们不能拥有operator==(C, C)
and operator==(A, B)
出于优化目的?
这是一个有效的模型,我们可以做到这一点。然而,C++ 并不是一个柏拉图式的现实。尽管概念尽最大努力只接受真正满足语义要求的类型,但它实际上无法实现这一目标。因此,如果我们只检查operator==(A, B)
and operator==(C, C)
,我们冒着这样的风险operator==(A, A)
and operator==(B, B)
做一些不同的事情。此外,如果我们能够拥有operator==(C, C)
,那么这意味着写起来很简单operator==(A, A)
and operator==(B, B)
基于我们所拥有的operator==(C, C)
。也就是说,要求的危害operator==(A, A)
and operator==(B, B)
相当低,作为回报,我们更有信心我们实际上是平等的。
然而,在某些情况下,这会遇到困难;看P2405 https://wg21.link/p2405.
多么累啊。我们不能只要求这个吗operator==(A, B)
是真正的平等吗?我永远不会真正使用operator==(A, A)
or operator==(B, B)
反正;我只关心能够进行跨类型比较。
实际上,我们需要的模型operator==(A, B)
是一个实际的平等可能会起作用。在这个模型下,我们将有std::equality_comparable_with<iterator, sentinel>
,但这在所有已知的背景下到底意味着什么,可以通过敲定来解决。然而,这不是标准所遵循的方向是有原因的,在人们了解是否或如何改变它之前,他们必须首先了解为什么选择标准的模型。