首页 > 解决方案 > 为什么在 C++20 中 unique_ptr 不是 equal_comparable_with nullptr_t?

问题描述

使用 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;

这个要求应该实现对称比较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 标准中的缺陷?

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

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

解决方案


TL;DR:std::equality_comparable_with<T, U>要求两者TU都可以转换为Tand的公共引用U。对于 and 的情况std::unique_ptr<T>std::nullptr_t这要求 thatstd::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>;

那是一口。将概念分解为各个部分,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>
      |                              ^~~~~~~~~~~~~~~~~~~~~~~~~~~~

(为便于阅读而编辑)编译器资源管理器链接

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>(参见编译器资源管理器链接)。

综上所述,有一个传递要求 that 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)说:

  • 如果T1andT2都是引用类型,并且存在 and 的简单公共引用类型 S(定义如下),那么成员类型 type names ;T1T2S
  • 否则,如果std::basic_common_reference<std::remove_cvref_t<T1>, std::remove_cvref_t<T2>, T1Q, T2Q>::type存在,其中TiQ是一个一元别名模板,例如添加' 的 cv- 和引用限定符,则成员类型类型命名该类型TiQ<U>UTi
  • 否则,如果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其中至少一个T1T2产生不同的类型,则成员类型命名与 相同的类型( 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 == u拥有t2 == 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太严格了。重要的是,数学要求只是存在一个共同的超类型,其中 this 提升operator==是一个相等,但共同的引用要求需要更严格的东西,另外还需要:

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

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

我们可以放宽公共引用要求以更紧密地遵循预期的公共超类型要求吗?

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

这可以通过更改 的std::common_reference_with部分来实现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>产生 or 的参考剥离版本,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用户定义类型(通常是代理引用)的结果。

这是一个可怕的 hack,但是如果我们编写一个支持我们想要比较的两种类型的代理引用,我们可以专门化std::basic_common_reference我们的类型,使我们的类型能够满足std::equality_comparable_with. 另请参阅如何告诉编译器 MyCustomType 是equality_comparable_with SomeOtherType?. 如果您选择这样做,请注意;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>对于and之间的公共引用custom_vector_ref<T>,甚至可能是custom_vector<T>and之间的公共引用,这可能是一个不错的选择std::array<T, N>。小心行事。

如何扩展我无法控制的类型std::equality_comparable_with

你不能。专门std::basic_common_reference针对您不拥有的类型(std::类型或某些第三方库)充其量是不好的做法,最坏的情况是未定义的行为。最安全的选择是使用您拥有的代理类型,您可以通过它进行比较,或者编写您自己的扩展,std::equality_comparable_with为您的自定义相等拼写具有明确的自定义点。


好的,我知道这些要求的概念是数学上的健全性,但是这些要求如何实现数学上的健全性,为什么它如此重要?

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

这意味着我们的关系c1 == c2无论从哪里来都必须被定义c1c2所以我们必须有a1 == a2, a == b, and b1 == b2(where aiis from Aand biis from B)。翻译成 C++,这意味着所有的operator==(A, A), operator==(A, B), operator==(B, B), andoperator==(C, C)必须是同一个等式的一部分。

这就是iterator/ sentinels 不满足的原因std::equality_comparable_with:虽然operator==(iterator, sentinel)实际上可能是某些等价关系的一部分,但它不是相同等价关系的一部分operator==(iterator, iterator)(否则迭代器相等只会回答“最后两个迭代器还是两个迭代器”的问题不是在最后吗?”)。

operator==写一个实际上不是相等的其实很容易,因为你必须记住,异质的相等不是operator==(A, B)你正在写的单一的,而是四个不同operator==的,它们都必须是内聚的。

等一下,为什么我们需要全部四个operator==s;为什么我们不能仅仅为了优化目的而拥有operator==(C, C)和?operator==(A, B)

这是一个有效的模型,我们可以这样做。然而,C++ 并不是一个柏拉图式的现实。尽管概念尽最大努力只接受真正满足语义要求的类型,但实际上并不能实现这一目标。因此,如果我们只检查operator==(A, B)and ,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>,但在所有已知的情况下,这意味着什么可以敲定。然而,这不是标准的发展方向是有原因的,在人们了解是否或如何改变它之前,他们必须首先了解为什么选择标准的模型。


推荐阅读