为什么在 C++20 中 unique_ptr 不是 equality_comparable_with nullptr_t?

Why is unique_ptr not equality_comparable_with nullptr_t in C++20?

使用 C++20 的 concepts 我注意到 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>);

Try Online

然而,接受与 std::shared_ptr 相同的断言:

// succeeds on all three compilers
static_assert(std::equality_comparable_with<std::shared_ptr<int>,std::nullptr_t>);

Try Online

除非我误解了什么,否则这似乎是一个错误。 我的问题是这是否是三个编译器实现中的巧合错误,或者这是 C++20 标准中的缺陷?

注意:我正在标记这个 以防这恰好是一个缺陷。

TL;DR:std::equality_comparable_with<T, U> 要求 TU 都可以转换为 TU 的公共引用。对于 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_typestd::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_withstd::totally_ordered_withstd::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== 是相等的,但公共参考要求要求更严格,另外要求:

  1. 公共超类型必须是通过std::common_reference_t获得的超类型。
  2. 我们必须能够形成一个共同的超类型 reference 这两种类型。

放松第一点基本上只是为 std::equality_comparable_with 提供一个明确的定制点,您可以在其中明确选择一对类型来满足这个概念。对于第二点,在数学上,“参考”是没有意义的。因此,第二点也可以放宽,以允许公共超类型可以从两种类型隐式转换。

我们能否放宽公共引用要求以更严格地遵循预期的公共超类型要求?

这很难做到正确。重要的是,我们实际上只关心公共超类型是否存在,但实际上我们从来不需要在代码中使用它。因此,我们无需担心效率问题,甚至在编写通用超类型转换时是否无法实现。

这可以通过更改 equality_comparable_withstd::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> 生成参考剥离版本的 TU 并同时尝试 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 扩展,它具有用于自定义相等拼写的显式自定义点。


好的,我知道这些要求的想法是数学稳健性,但这些要求如何实现数学稳健性,为什么它如此重要?

在数学上,相等是一种等价关系。但是,等价关系是在单个集合上定义的。那么我们如何定义两个集合AB之间的等价关系呢?简单地说,我们改为在 C = A∪B 上定义等价关系。也就是说,我们取 AB 的公共超类型,并在这个超类型上定义等价关系。

这意味着无论c1c2来自哪里,我们的关系c1 == c2都必须定义,所以我们必须有a1 == a2a == bb1 == b2(其中 ai 来自 Abi 来自 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>,但是在所有已知的上下文中这到底意味着什么可以敲定。然而,这不是标准的方向是有原因的,在理解是否或如何改变它之前,他们必须首先理解为什么选择标准的模型。