【问题标题】:Why is unique_ptr not equality_comparable_with nullptr_t in C++20?为什么在 C++20 中 unique_ptr 不是 equal_comparable_with nullptr_t?
【发布时间】:2021-06-30 10:12:18
【问题描述】:

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

此要求应该实现与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标准中的缺陷?

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

【问题讨论】:

  • "据我了解足以满足equality_comparable_with" 不是,但我没有看到任何其他不满足的要求。跨度>

标签: language-lawyer c++ language-lawyer c++20 c++-concepts


【解决方案1】:

TL;DR:std::equality_comparable_with&lt;T, U&gt; 要求 TU 都可以转换为 TU 的公共引用。对于std::unique_ptr&lt;T&gt;std::nullptr_t 的情况,这要求std::unique_ptr&lt;T&gt; 是可复制构造的,但事实并非如此。


系好安全带。这真是一段旅程。考虑我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&lt;std::unique_ptr&lt;int&gt;, std::nullptr_t&gt; 失败,std::common_reference_with&lt;const std::unique_ptr&lt;int&gt;&amp;, const std::nullptr_t&amp;&gt;

<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&lt;const std::unique_ptr&lt;int&gt;&amp;, const std::nullptr_t&amp;&gt;std::unique_ptr&lt;int&gt;(参见 compiler explorer link)。

综上所述,有一个传递要求 std::convertible_to&lt;const std::unique_ptr&lt;int&gt;&amp;, std::unique_ptr&lt;int&gt;&gt;,相当于要求 std::unique_ptr&lt;int&gt; 是可复制构造的。

为什么std::common_reference_t 不是参考?

为什么是std::common_reference_t&lt;const std::unique_ptr&lt;T&gt;&amp;, const std::nullptr_t&amp;&gt; = std::unique_ptr&lt;T&gt; 而不是const std::unique_ptr&lt;T&gt;&amp;std::common_reference_t 的两种类型(sizeof...(T) 是两种)的文档说:

  • 如果T1T2 都是引用类型,并且T1T2简单公共引用类型 S(定义如下)存在,那么 成员类型类型名称S;
  • 否则,如果std::basic_common_reference&lt;std::remove_cvref_t&lt;T1&gt;, std::remove_cvref_t&lt;T2&gt;, T1Q, T2Q&gt;::type 存在,其中TiQ 是一元 别名模板,使得 TiQ&lt;U&gt;U 加上 Ti 的 cv- 和 引用限定符,然后是该类型的成员类型类型名称;
  • 否则,如果decltype(false? val&lt;T1&gt;() : val&lt;T2&gt;())(其中 val 是函数模板template&lt;class T&gt; T val();)是有效类型,则 成员类型类型名称该类型;
  • 否则,如果std::common_type_t&lt;T1, T2&gt; 是有效类型,则成员类型类型命名该类型;
  • 否则,没有成员类型。

const std::unique_ptr&lt;T&gt;&amp;const std::nullptr_t&amp; 没有简单的公共引用类型,因为引用不能立即转换为公共基本类型(即false ? crefUPtr : crefNullptrT 格式错误)。 std::unique_ptr&lt;T&gt; 没有 std::basic_common_reference 专业化。第三个选项也失败了,但是我们触发了std::common_type_t&lt;const std::unique_ptr&lt;T&gt;&amp;, const std::nullptr_t&amp;&gt;

对于std::common_typestd::common_type&lt;const std::unique_ptr&lt;T&gt;&amp;, const std::nullptr_t&amp;&gt; = std::common_type&lt;std::unique_ptr&lt;T&gt;, std::nullptr_t&gt;,因为:

如果将std::decay 应用于T1T2 中的至少一个会产生一个 不同的类型,成员类型名称相同的类型 std::common_type&lt;std::decay&lt;T1&gt;::type, std::decay&lt;T2&gt;::type&gt;::type,如果 它存在;如果没有,则没有成员类型。

std::common_type&lt;std::unique_ptr&lt;T&gt;, std::nullptr_t&gt; 确实存在;它是std::unique_ptr&lt;T&gt;。这就是引用被剥离的原因。


我们可以修复标准以支持这样的案例吗?

这变成了P2404,它提议对std::equality_comparable_withstd::totally_ordered_withstd::three_way_comparable_with 进行更改以支持仅移动类型。

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

Does `equality_­comparable_with` need to require `common_reference`? 中,justification given by T.C.(最初来自 n3351 第 15-16 页)对于 equality_comparable_with 的通用参考要求是:

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

仅仅要求== 操作可能会天真地期望这个概念是行不通的,因为:

[I]t 允许 t == ut2 == ut != 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== 是一个等式,但共同的引用要求要求更严格,另外还要求:

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

放宽第一点基本上只是为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&lt;T, U&gt; 生成TU 的引用剥离版本,并且还通过尝试两者C(T&amp;&amp;)C(const T&amp;) 创建公共引用。详情请见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 对用户定义类型(通常是代理引用)的结果。

这是一个可怕的 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&lt;T&gt; 可能是custom_vector&lt;T&gt;custom_vector_ref&lt;T&gt; 之间的公共引用的好选择,甚至可能是custom_vector&lt;T&gt;std::array&lt;T, N&gt; 之间的公共引用。小心行事。

如何扩展我无法控制的类型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/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==(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&lt;iterator, sentinel&gt;,但在所有已知上下文中这意味着什么可以敲定。然而,这不是标准的发展方向是有原因的,在人们了解是否或如何改变它之前,他们必须首先了解为什么选择标准的模型。

【讨论】:

  • @Human-Compiler 我不会假装理解标准或std::equality_comparable_with 具有common_reference 要求的原因,但我确实认为这是标准中的缺陷。
  • @Human-Compiler:就个人而言,我认为整个common_reference requirement of equality_comparable_with 是有缺陷的,但我非常怀疑它会被改变。
  • 是我自己,还是语言慢慢地向语言律师的游乐场漂移,同时以安全的方式变得几乎无法使用(因为通常不可能理解给定的代码在做什么)?
  • @Peter-ReinstateMonica 只有当你把这些微小的细节看得太大时,它才会看起来如此。当然,如果这个角落案例能按预期工作,那就太好了。但总的来说,我认为 C++ 正朝着成为一种更容易使用、更安全的语言方向发展。
  • @G.Sliepen 令人惊讶的是,并非所有人都能立即了解它在所有可能的情况下的工作原理。多年来一直在编写 C++ 代码的专业人士,如果想要获得这种程度的理解,每次新标准问世时都必须花费数百小时来学习。这是完全不合理的。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2014-08-26
  • 2020-08-06
  • 1970-01-01
  • 2018-02-25
  • 2020-10-29
相关资源
最近更新 更多