当前位置: 首页 > 知识库问答 >
问题:

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

楮乐邦
2023-03-14

使用C++20的概念时,我注意到std::unique_ptr似乎不能满足std::equality_comparable_with 概念。根据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>);

联机尝试

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

注意:我将这个语言标记为律师,以防这碰巧是一个缺陷。

共有1个答案

郏博瀚
2023-03-14

tl;dr:std::equality_comparable_with 要求tu都可以转换为tu的公共引用。对于std::unique_ptr std::nullptr_t的情况,这要求std::unique_ptr 是可复制构造的,但事实并非如此。

扣上。这趟旅程真不错。就当我是书呆子吧。

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::nullptr_t> std::common_reference_with &,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::nullptr_t&> std::unique_ptr (请参阅编译器资源管理器链接)。

综合起来,有一个传递性要求,即std::convertible_to &,std::unique_ptr ,这相当于要求std::unique_ptr 是可复制构造的。

为什么std::common_reference_t &,const std::nullptr_t&>=std::unique_ptr 而不是const std::unique_ptr & ?两种类型的std::common_reference_t文档(sizeof...(T)是两个)说明如下:

  • 如果T1T2都是引用类型,并且T1T2(如下所定义)的简单公共引用类型S存在,则成员类型类型名为S
  • 否则,如果std::basic_common_reference ,std::remove_cvref_t ,T1Q,t2q>::type 存在,其中tiq是一元别名模板,因此tiqu加上了ti的cv-和引用限定符,则成员类型为该类型命名;
  • 否则,如果decltype(false?val ():val ()) (其中val是函数模板模板 T val(); )是有效类型,则成员类型命名该类型;
  • 否则,如果std::common_type_t 是有效类型,则成员类型为该类型命名;
  • 否则,没有成员类型。

const std::unique_ptr & const std::nullptr_t&没有简单的公共引用类型,因为引用不能立即转换为公共基类型(即为假?crefUPtr:crefnullptrt格式不正确)。对于std::unique_ptr 没有std::basic_common_reference专门化。第三个选项也失败,但我们触发std::common_type_t &,const std::nullptr_t&>

对于std::common_typestd::common_type &,const std::nullptr_t&>=std::common_type ,std::nullptr_t> ,因为:

如果将std::decay应用于t1t2中的至少一个产生不同的类型,则成员类型将与std::common_type ::type、std::decay :type>::type 相同的类型命名,如果存在;如果没有,则没有成员类型。

std::common_type ,std::nullptr_t> 实际上确实存在;它是std::unique_ptr 。这就是引用被剥离的原因。

在'equality_comparable_with'是否需要'common_reference'一书中,T.C.(最初来源于n3351第15-16页)关于equality_comparable_with的公共参考要求是:

这是否意味着两个不同类型的值相等?设计中说,跨类型相等是通过将它们映射到公共(引用)类型来定义的(为了保留值,需要进行这种转换)。

仅仅需要==操作是不起作用的,这可能是因为:

[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表达了这种异类相等已经是相等的扩展,它只是在单个类型内严格地数学定义的。事实上,当我们编写异类相等运算时,我们假装这两种类型共享一个公共的超级类型,运算发生在该公共类型内。

可能std::equality_comparable的公共引用要求过于严格;如果使用const common_ref_t&(允许引用折叠),上面的代码实际上可以工作,因为std::convertible_to =std::convertible_to &> 是true,因为我们可以使用std::unique_ptr 的隐式构造函数并html" target="_blank">执行临时生存期扩展。

如果我们修改std::common_reference_with来支持这种情况,那么我们将是黄金。由于std::common_reference_with构建于std::common_reference_t之上,所以问题变成了,“我们可以修复std::common_reference_t吗?”

如果为std::nullptr_tstd::unique_ptr添加了basic_common_reference的特化,则std::equality_comparable_with ,std::nullptr_t> 为true:

namespace std {
template <typename T, typename D, template <typename> class UPtrQual, template <typename> class NullptrQual>
struct basic_common_reference<std::unique_ptr<T, D>, std::nullptr_t, UPtrQual, NullptrQual> {
    using type = UPtrQual<NullptrQual<std::unique_ptr<T, D>>>;
};
template <typename T, typename D, template <typename> class UPtrQual, template <typename> class NullptrQual>
struct basic_common_reference<std::nullptr_t, std::unique_ptr<T, D>, NullptrQual, UPtrQual> {
    using type = UPtrQual<NullptrQual<std::unique_ptr<T, D>>>;
};
}

(编译器资源管理器链接)

请注意std::common_reference:

确定类型T...的公共引用类型,即T...中的所有类型都可以转换或绑定到的类型。

可以认为,这将为std::common_reference_tconst&&&const&&情况的std:::common_reference_t的专门化提供理由,其中std::nullptr_t将创建绑定到引用的临时std::unique_ptr std::common_reference_t。但是,如果它是std::unique_ptr & ,这种专门化是不正确的,因为它不能绑定到rvalue。

对于std::shared_ptr 也有必要进行专门化,因为此时-std::common_reference_t &,const std::nullptr_t&> const std::shared_ptr ,这意味着类似于上述转换为公共引用相等的通用代码可能会不必要地复制std::shared_ptr ,从而添加不必要的原子增量/减量。

std::common_reference -用于参考tu-可以扩展,以支持std::convertible_to std::convertible_to 为true的情况,即可以通过临时时间延长临时生存期。这将避免对unique_ptr或具有相同问题的任何自定义用户类型进行专门化。

正如T.C.所提到的。在评论中,这是相当危险的。如果我们支持临时生存期扩展,那么编写带有引用的外观清白的代码就会变得非常容易。在泛型代码中,这将意味着添加另一个必须了解和考虑的情况,否则将冒着仅对所有类型的一个小子集显示的微妙的未定义行为的风险。

请注意,std::equality_comparable_with在总体方案中并不重要。faik,每个具有std::equality_comparable_with约束的标准库函数都有一个没有该约束的谓词版本,这意味着您可以只使用std::equal_to()或自定义lambda。

即使有可能的放松,假设没有公共的引用要求就不可能保持数学的严密性,std::equality_comparable_with将始终无法支持某些类型,这些类型在假装为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是equality_comparable_with SomeOtherType?.

这可以为标准类型实现,也可以为任何示例实现,在这些示例中,我们具有异类等式,并且默认情况下没有获得良好的公共引用,例如std::unique_ptr +std::nullptr_t 情况。

你不能。为您不拥有的类型(std::basic_common_reference类型或某些第三方库)专用化std::充其量是错误的做法,最糟糕的是未定义的行为。但是,我们可以使用包装器类型和自定义概念,而不是std::equality_comparable_with:

#include <utility>

// From https://en.cppreference.com/w/cpp/concepts/boolean-testable
template<class B>
concept boolean_testable_impl = std::convertible_to<B, bool>;

template<class B>
concept boolean_testable =
    boolean_testable_impl<B> &&
    requires (B&& b) {
        { !std::forward<B>(b) } -> boolean_testable_impl;
    };

// From https://en.cppreference.com/w/cpp/concepts/equality_comparable
template<class T, class U>
concept weakly_equality_comparable_with =
    requires(const std::remove_reference_t<T>& t,
             const std::remove_reference_t<U>& u) {
      { t == u } -> boolean_testable;
      { t != u } -> boolean_testable;
      { u == t } -> boolean_testable;
      { u != t } -> boolean_testable;
    };

// The actual additional type and concept
template <typename T>
class eq_wrapper {
private:
    const T& ref_;

public:
    template <typename U>
        requires std::convertible_to<U, T>
    constexpr eq_wrapper(U ref)
        : ref_(ref)
    {}

    template <typename U>
        requires weakly_equality_comparable_with<T, U>
    constexpr bool operator==(const eq_wrapper<U>& rhs) const {
        return ref_ == rhs.ref_;
    };
};

template <typename T, typename U>
concept wrapped_equality_comparable_with =
    // To give us subsumption with std::equality_comparable_with:
    std::equality_comparable_with<T, U> ||
    // To support when we're not actually equality_comparable_with
    std::equality_comparable_with<eq_wrapper<T>, eq_wrapper<U>> &&
    weakly_equality_comparable_with<T, U>;

若要支持wrapped_equality_comparable_with ,您应该为eq_wrapper eq_wrapper实现上述扩展(注意:这不应该为任何弱可比性的tu实现,而只应该为具有真相等性的类型实现)

 类似资料:
  • C++20引入了“destroying”(带有标记类型),是的新重载。 这到底是什么,什么时候有用?

  • 问题内容: 我们尝试使用以下Java代码从字符串转换为: 我们得到一个长度为22个字节的字节数组,我们不确定此填充来自何处。如何获得长度为20的数组? 问题答案: 亚历山大(Alexander)的答案解释了为什么存在它,但没有解释如何摆脱它。您只需要在编码名称中指定所需的字节序即可:

  • 当我今天阅读C标准时,它提到了副作用 访问易失性对象、修改对象、修改文件或调用执行任何这些操作的函数都是副作用 C标准说 访问易失性glvalue(3.10)指定的对象、修改对象、调用库I/O函数或调用执行任何这些操作的函数都是副作用 因此,因为两者都禁止在同一个标量对象上发生未排序的副作用,所以C允许以下内容,但C使其成为未定义的行为 我是否正确阅读了规格?如果存在差异,原因是什么?

  • 当我只运行预处理器时,输出文件包含20。 然而,据我所知,预处理器只是进行文本替换。所以这就是我认为正在发生的事情(这显然是错误的,但idky): NUM被定义为10 所以我认为输出应该是10而不是20。有什么能解释出哪里出了问题吗?

  • 代码显示了我的问题,我不能在。 错误消息是: /home/linuxbrew/。linuxbrew/Cellar/gcc/11.1。0_1/include/c/11.1。0/范围:1775:48:错误:传递'std::ranges::take_view