为什么在 C++20 中 unique_ptr 不是 equality_comparable_with nullptr_t?
Why is unique_ptr not equality_comparable_with nullptr_t in C++20?
使用 C++20 的 concept
s 我注意到 std::unique_ptr
似乎无法满足 std::equality_comparable_with<std::nullptr_t,...>
concept. From 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;
这个要求应该实现与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>);
然而,接受与 std::shared_ptr
相同的断言:
// succeeds on all three compilers
static_assert(std::equality_comparable_with<std::shared_ptr<int>,std::nullptr_t>);
除非我误解了什么,否则这似乎是一个错误。
我的问题是这是否是三个编译器实现中的巧合错误,或者这是 C++20 标准中的缺陷?
注意:我正在标记这个 language-lawyer 以防这恰好是一个缺陷。
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 link.
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>&
? std::common_reference_t
两种类型(sizeof...(T)
是两种)的文档说:
- If
T1
and T2
are both reference types, and the simple common reference type S
of T1
and T2
(as defined below) exists, then the
member type type names S
;
- Otherwise, if
std::basic_common_reference<std::remove_cvref_t<T1>, std::remove_cvref_t<T2>, T1Q, T2Q>::type
exists, where TiQ
is a unary
alias template such that TiQ<U>
is U
with the addition of Ti
's cv- and
reference qualifiers, then the member type type names that type;
- Otherwise, if
decltype(false? val<T1>() : val<T2>())
, where val is a function template template<class T> T val();
, is a valid type, then
the member type type names that type;
- Otherwise, if
std::common_type_t<T1, T2>
is a valid type, then the member type type names that type;
- Otherwise, there is no member type.
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>
,因为:
If applying std::decay
to at least one of T1
and T2
produces a
different type, the member type names the same type as
std::common_type<std::decay<T1>::type, std::decay<T2>::type>::type
, if
it exists; if not, there is no member 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
以支持仅移动类型。
为什么我们有这些共同参考要求?
在 , the (originally sourced from n3351 第 15-16 页)中 equality_comparable_with
的共同参考要求是:
[W]hat does it even mean for two values of different types to be equal? The design says that cross-type equality is defined by mapping them to the common (reference) type (this conversion is required to preserve the value).
仅要求可能天真地期望该概念的 ==
操作不起作用,因为:
[I]t allows having 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
的共同参考要求太严格了。重要的是,数学上的要求只是存在一个公共超类型,其中 lifted operator==
是相等的,但公共参考要求要求更严格,另外要求:
- 公共超类型必须是通过
std::common_reference_t
获得的超类型。
- 我们必须能够形成一个共同的超类型 reference 这两种类型。
放松第一点基本上只是为 std::equality_comparable_with
提供一个明确的定制点,您可以在其中明确选择一对类型来满足这个概念。对于第二点,在数学上,“参考”是没有意义的。因此,第二点也可以放宽,以允许公共超类型可以从两种类型隐式转换。
我们能否放宽公共引用要求以更严格地遵循预期的公共超类型要求?
这很难做到正确。重要的是,我们实际上只关心公共超类型是否存在,但实际上我们从来不需要在代码中使用它。因此,我们无需担心效率问题,甚至在编写通用超类型转换时是否无法实现。
这可以通过更改 equality_comparable_with
的 std::common_reference_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()
传递给谓词重载并获得所需的行为(not std::ranges::equal_to
,这是受约束的,但不受约束的 std::equal_to
).
但这并不意味着不修复 std::equality_comparable_with
是个好主意。
我可以扩展自己的类型以满足 std::equality_comparable_with
吗?
通用参考要求使用 std::common_reference_t
,其自定义点为 std::basic_common_reference
,目的是:
The class template basic_common_reference
is a customization point that allows users to influence the result of common_reference
for user-defined types (typically proxy references).
这是一个可怕的 hack,但如果我们编写一个支持我们想要比较的两种类型的代理引用,我们可以为我们的类型特化 std::basic_common_reference
,使我们的类型满足 std::equality_comparable_with
。另见 How can I tell the compiler that MyCustomType is equality_comparable_with SomeOtherType? 。如果您选择这样做,请当心; std::common_reference_t
不仅被 std::equality_comparable_with
或其他 <i>comparison_relation</i>_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::basic_common_reference
专门用于您不拥有的类型(std::
类型或某些第三方库)充其量是不好的做法,最坏的情况是未定义的行为。最安全的选择是使用您拥有的代理类型,您可以通过它进行比较,或者编写您自己的 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)
的危害非常低,并且在 return 中我们更有信心我们实际上是平等的。
然而,在某些情况下,运行会变成粗糙的边缘;参见 P2405。
好累啊。我们不能只要求 operator==(A, B)
是一个真正的平等吗?无论如何,我永远不会真正使用 operator==(A, A)
或 operator==(B, B)
;我只关心能够进行跨类型比较。
实际上,我们需要 operator==(A, B)
的模型是实际相等的可能会起作用。在这个模型下,我们会有 std::equality_comparable_with<iterator, sentinel>
,但是在所有已知的上下文中这到底意味着什么可以敲定。然而,这不是标准的方向是有原因的,在理解是否或如何改变它之前,他们必须首先理解为什么选择标准的模型。
使用 C++20 的 concept
s 我注意到 std::unique_ptr
似乎无法满足 std::equality_comparable_with<std::nullptr_t,...>
concept. From 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;
这个要求应该实现与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>);
然而,接受与 std::shared_ptr
相同的断言:
// succeeds on all three compilers
static_assert(std::equality_comparable_with<std::shared_ptr<int>,std::nullptr_t>);
除非我误解了什么,否则这似乎是一个错误。 我的问题是这是否是三个编译器实现中的巧合错误,或者这是 C++20 标准中的缺陷?
注意:我正在标记这个 language-lawyer 以防这恰好是一个缺陷。
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 link.
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>&
? std::common_reference_t
两种类型(sizeof...(T)
是两种)的文档说:
- If
T1
andT2
are both reference types, and the simple common reference typeS
ofT1
andT2
(as defined below) exists, then the member type type namesS
;- Otherwise, if
std::basic_common_reference<std::remove_cvref_t<T1>, std::remove_cvref_t<T2>, T1Q, T2Q>::type
exists, whereTiQ
is a unary alias template such thatTiQ<U>
isU
with the addition ofTi
's cv- and reference qualifiers, then the member type type names that type;- Otherwise, if
decltype(false? val<T1>() : val<T2>())
, where val is a function templatetemplate<class T> T val();
, is a valid type, then the member type type names that type;- Otherwise, if
std::common_type_t<T1, T2>
is a valid type, then the member type type names that type;- Otherwise, there is no member type.
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>
,因为:
If applying
std::decay
to at least one ofT1
andT2
produces a different type, the member type names the same type asstd::common_type<std::decay<T1>::type, std::decay<T2>::type>::type
, if it exists; if not, there is no member 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
的共同参考要求是:
[W]hat does it even mean for two values of different types to be equal? The design says that cross-type equality is defined by mapping them to the common (reference) type (this conversion is required to preserve the value).
仅要求可能天真地期望该概念的 ==
操作不起作用,因为:
[I]t allows having
t == u
andt2 == u
butt != 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
的共同参考要求太严格了。重要的是,数学上的要求只是存在一个公共超类型,其中 lifted operator==
是相等的,但公共参考要求要求更严格,另外要求:
- 公共超类型必须是通过
std::common_reference_t
获得的超类型。 - 我们必须能够形成一个共同的超类型 reference 这两种类型。
放松第一点基本上只是为 std::equality_comparable_with
提供一个明确的定制点,您可以在其中明确选择一对类型来满足这个概念。对于第二点,在数学上,“参考”是没有意义的。因此,第二点也可以放宽,以允许公共超类型可以从两种类型隐式转换。
我们能否放宽公共引用要求以更严格地遵循预期的公共超类型要求?
这很难做到正确。重要的是,我们实际上只关心公共超类型是否存在,但实际上我们从来不需要在代码中使用它。因此,我们无需担心效率问题,甚至在编写通用超类型转换时是否无法实现。
这可以通过更改 equality_comparable_with
的 std::common_reference_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()
传递给谓词重载并获得所需的行为(not std::ranges::equal_to
,这是受约束的,但不受约束的 std::equal_to
).
但这并不意味着不修复 std::equality_comparable_with
是个好主意。
我可以扩展自己的类型以满足 std::equality_comparable_with
吗?
通用参考要求使用 std::common_reference_t
,其自定义点为 std::basic_common_reference
,目的是:
The class template
basic_common_reference
is a customization point that allows users to influence the result ofcommon_reference
for user-defined types (typically proxy references).
这是一个可怕的 hack,但如果我们编写一个支持我们想要比较的两种类型的代理引用,我们可以为我们的类型特化 std::basic_common_reference
,使我们的类型满足 std::equality_comparable_with
。另见 How can I tell the compiler that MyCustomType is equality_comparable_with SomeOtherType? 。如果您选择这样做,请当心; std::common_reference_t
不仅被 std::equality_comparable_with
或其他 <i>comparison_relation</i>_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::basic_common_reference
专门用于您不拥有的类型(std::
类型或某些第三方库)充其量是不好的做法,最坏的情况是未定义的行为。最安全的选择是使用您拥有的代理类型,您可以通过它进行比较,或者编写您自己的 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)
的危害非常低,并且在 return 中我们更有信心我们实际上是平等的。
然而,在某些情况下,运行会变成粗糙的边缘;参见 P2405。
好累啊。我们不能只要求 operator==(A, B)
是一个真正的平等吗?无论如何,我永远不会真正使用 operator==(A, A)
或 operator==(B, B)
;我只关心能够进行跨类型比较。
实际上,我们需要 operator==(A, B)
的模型是实际相等的可能会起作用。在这个模型下,我们会有 std::equality_comparable_with<iterator, sentinel>
,但是在所有已知的上下文中这到底意味着什么可以敲定。然而,这不是标准的方向是有原因的,在理解是否或如何改变它之前,他们必须首先理解为什么选择标准的模型。