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

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)

Try Online

然而,同样的断言与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)

Try Online

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

注意:如果这恰好是一个缺陷,我会标记这个

Jus*_*tin 60

TL; DR:std::equality_comparable_with<T, U>需要两个TU可转换到的所述公共参考TU。对于std::unique_ptr<T>and的情况std::nullptr_t,这要求它std::unique_ptr<T>是可复制构造的,而事实并非如此。


系好安全带。这真是一段旅程。把我当成书呆子狙击手吧

为什么我们不满足这个概念?

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>;
Run Code Online (Sandbox Code Playgroud)

那是一口。将概念分解成各个部分,std::equality_comparable_with<std::unique_ptr<int>, std::nullptr_t>失败了std::common_reference_with<const std::unique_ptr<int>&, const std::nullptr_t&>

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>;
Run Code Online (Sandbox Code Playgroud)

(为了易读性而编辑)编译器资源管理器链接

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>>;
Run Code Online (Sandbox Code Playgroud)

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)是两个)的文档说:

  • 如果T1T2都引用类型,和简单的公共参考类型 ST1T2(如下面所定义)存在,则该成员类型类型名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_withstd::totally_ordered_with以及std::three_way_comparable_with只支持移动类型。

为什么我们甚至有这些共同参考要求?

`equality_comparable_with` 是否需要要求`common_reference`?TC(最初来自n3351第 15-16 页)对公共参考要求给出理由equality_comparable_with是:

[W]两个不同类型的值相等意味着什么?该设计表示跨类型相等是通过将它们映射到公共(引用)类型来定义的(需要这种转换来保留值)。

仅仅要求==可能对概念天真地预期的操作是行不通的,因为:

[I]t 允许拥有t == ut2 == 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==是一个平等,但共同的参考要求要求更严格,另外要求:

  1. 公共超类型必须是通过std::common_reference_t.
  2. 我们必须能够形成对这两种类型的公共超类型引用

放松第一点基本上只是提供一个明确的定制点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必须无论身在何处定义c1c2从何而来,所以我们必须有a1 == a2a == bb1 == b2(其中aiAbi来自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>,但是在所有已知的上下文中这究竟意味着什么可以敲定。然而,这不是标准的方向是有原因的,在了解是否或如何改变它之前,他们必须首先了解为什么选择标准的模型。

  • 是只有我一个人还是这种语言正在慢慢地走向语言律师的游乐场,同时变得几乎无法以安全的方式使用(因为通常不可能理解给定的代码片段在做什么)? (13认同)
  • @Human-Compiler我不会假装理解标准或“std::equality_comparable_with”具有“common_reference”要求的原因,但我确实认为这是标准中的缺陷。 (6认同)
  • @Human-Compiler:就我个人而言,我认为`equality_comparable_with`的整个[`common_reference`要求](/sf/ask/4282411171/参考)有缺陷,但我非常怀疑它会被改变。 (6认同)
  • @Peter-ReinstateMonica 只有当你把这些微小的细节看得太重要时,它才会看起来像这样。当然,如果这个极端情况能像预期的那样工作得更好,那就太好了。但总的来说,我认为 C++ 正逐渐成为一种更容易使用、更安全的语言。 (3认同)
  • @G.Sliepen 令人惊讶的是,并不是每个人都能立即理解它在所有可能场景中的工作原理。多年来一直编写 C++ 代码的专业人士如果想要达到这种程度的理解,就必须在每次新标准出现时投入数百个小时来学习。这完全不合理。 (2认同)