TL;DR:
std::equality_comparable_with<T, U>
要求
T
和
U
都可以转换为
T
和
U
的公共引用。对于
std::unique_ptr<T>
和
std::nullptr_t
的情况,这要求
std::unique_ptr<T>
是可复制构造的,但实际上它不是可复制构造的。
请系好安全带,这将是一次惊险的旅程。把我看作一个nerd-sniped。
为什么我们不能满足概念?
std::equality_comparable_with
要求:
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>
| ^~~~~~~~~~~~~~~~~~~~~~~~~~~~
这是Compiler Explorer链接。
std::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&>
是 std::unique_ptr<int>
(请参见compiler explorer link)。
综合考虑,有一个传递性要求,即std::convertible_to<const std::unique_ptr<int>&, std::unique_ptr<int>>
,这等价于要求std::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>&
?对于两个类型(sizeof...(T)
为两个)的std::common_reference_t
的文档如下:
- 如果
T1
和T2
都是引用类型,并且T1
和T2
的简单公共引用类型S
存在(如下所定义),则成员类型类型名为S
;
- 否则,如果存在
std::basic_common_reference<std::remove_cvref_t<T1>, std::remove_cvref_t<T2>, T1Q, T2Q>::type
,其中TiQ
是一个一元别名模板,使得TiQ<U>
是添加了Ti
的cv和引用限定符的U
,则成员类型类型名为该类型;
- 否则,如果
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::unique_ptr<T>
,没有std::basic_common_reference
专门化。第三个选项也失败了,但我们触发了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
进行更改以支持仅移动类型。
我们为什么需要这些常见引用要求?
在Does `equality_comparable_with` need to require `common_reference`?中,T.C.给出的证明(最初源自n3351第15-16页)对于equality_comparable_with
上的常见引用要求是:
[两个不同类型的值相等]甚至是什么意思?设计表示,交叉类型的相等由将它们映射到公共(引用)类型来定义(此转换需要保留值)。
仅要求可能被概念天真地期望的==
操作是行不通的,因为:
[它]允许有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;
使用C++0X concepts n3351支持的内容,如果没有异构
operator==(T, U)
,则会使用此实现作为回退。在C++20 concepts中,我们要求存在异构
operator==(T, U)
,因此永远不会使用此实现。
请注意,n3351表达了这种异构相等已经是相等的扩展,而严格数学上只在单个类型内定义。事实上,当我们编写异构相等操作时,我们假设两种类型共享一个公共超类型,并在该公共类型内进行操作。
可以通过更改
equality_comparable_with
中的
std::common_reference_with
来实现此目标。对于
std::equality_comparable
的通用引用要求可能过于严格。重要的是,数学要求仅需要存在一个公共超类型,在其中此提升的
operator==
是相等的,但是通用引用要求需要更严格,还需要以下要求:
- 公共超类型必须是通过
std::common_reference_t
获得的。
- 我们必须能够形成两种类型的公共超类型引用。
放宽第一点基本上只是提供了
std::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
,其中__CommonSupertypeWith
的不同之处在于允许std::common_reference_t<T, U>
产生一个去除引用的T
或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与SomeOtherType是equality_comparable_with?。如果您选择这样做,请注意;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>
和 custom_vector_ref<T>
之间的通用引用,甚至可以在 custom_vector<T>
和 std::array<T, N>
之间使用。但需要小心谨慎。
如何扩展我无法控制的类型 std::equality_comparable_with
?
你无法这样做。为你不拥有的类型(无论是 std::
类型还是第三方库)专门化 std::basic_common_reference
最多是不良实践,最坏的情况下是未定义行为。最安全的选择是使用你自己拥有的代理类型来进行比较,或者编写自己的 std::equality_comparable_with
扩展,其中包含一个显式的定制点,用于你自定义的等号拼写。
好的,我明白这些要求的理念是数学上的正确性,但这些要求如何实现数学上的正确性,为什么它如此重要呢?
从数学上讲,等式是一种等价关系。然而,等价关系是在单个集合上定义的。那么我们如何在两个集合A和B之间定义一个等价关系?简单地说,我们改为在C=A∪B上定义等价关系。也就是说,我们取A和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/sentinel不符合std::equality_comparable_with的原因:虽然operator==(iterator, sentinel)实际上可能是某个等价关系的一部分,但它不是与operator==(iterator, iterator)相同的等价关系的一部分(否则迭代器相等只回答“两个迭代器是否都在末尾或两个迭代器都不在末尾?”的问题)。
实际上很容易编写一个operator==,它实际上并不是相等的,因为你必须记住异构相等不是你正在编写的单个operator==(A, B),而是四个不同的operator==,它们必须都是连贯的。
等一下,为什么我们需要所有四个operator==?我们为什么不能只有operator==(C, C)和operator==(A, B)来进行优化?
这是一个有效的模型,我们可以这样做。然而,C++并不是一个纯粹的现实。尽管概念尽力只接受真正满足语义要求的类型,但它实际上无法实现这个目标。因此,如果我们只检查
operator==(A, B)
和
operator==(C, C)
,我们就会冒着
operator==(A, A)
和
operator==(B, B)
执行不同操作的风险。此外,如果我们可以有
operator==(C, C)
,那么这意味着基于
operator==(C, C)
编写
operator==(A, A)
和
operator==(B, B)
是微不足道的。也就是说,要求
operator==(A, A)
和
operator==(B, B)
的危害非常低,换取的是我们确实具有相等性的更高信心。
然而,在某些情况下,这可能会遇到困难; 请参见P2405。
太麻烦了。我们不能只要求operator==(A, B)
是一个真正的相等吗?我永远不会实际使用operator==(A, A)
或operator==(B, B)
; 我只关心能够进行跨类型比较。
实际上,如果我们要求operator==(A, B)
是一个真正的相等,这个模型可能会起作用。在这种模型下,我们将拥有std::equality_comparable_with<iterator, sentinel>
,但在所有已知情况下,它确切意味着仍需澄清。然而,标准没有选择这个方向,这是有原因的。在理解是否以及如何更改之前,必须先理解标准选择该模型的原因。
equality_comparable_with
的要求。”实际上并不足够,但我没有看到其他未满足的要求。 - Nicol Bolas