为什么 std::unique_ptr 运算符*抛出而运算符>不抛出?

Why does std::unique_ptr operator* throw and operator-> does not throw?

本文关键字:运算符 gt std unique ptr 为什么      更新时间:2023-10-16

在c++标准草案(N3485)中,它声明如下:

20.7.1.2.4 unique_ptr observers [unique.ptr.single.observers]

typename add_lvalue_reference<T>::type operator*() const;
1 Requires: get() != nullptr.
2 Returns: *get().
pointer operator->() const noexcept;
3 Requires: get() != nullptr.
4 Returns: get().
5 Note: use typically requires that T be a complete type.

您可以看到operator*(解引用)没有被指定为noexcept,可能是因为它可能导致段故障,但是在同一对象上的operator->被指定为noexcept。两者的需求是相同的,但是在异常规范方面存在差异。

我注意到它们有不同的返回类型,一个返回指针,另一个返回引用。这是不是说operator->实际上没有引用任何东西?

问题的事实是,在任何类型的指针上使用operator->是NULL,将段故障(是UB)。那么,为什么其中一个被指定为noexcept而另一个没有呢?

我肯定我忽略了什么。

编辑:

std::shared_ptr我们有这个:

20.7.2.2.5 shared_ptr观察员[util.smartptr.shared.obs]

T& operator*() const noexcept;
T* operator->() const noexcept;

不一样吗?这和不同的所有权语义有关系吗?

段错误不在c++的异常系统中。如果你解引用一个空指针,你不会得到任何类型的异常抛出(好吧,至少如果你遵守Require:子句;

对于operator->,它通常被简单地实现为return m_ptr;(或unique_ptrreturn get();)。如您所见,操作符本身不能抛出——它只返回指针。没有引用,什么都没有。该语言对p->identifier有一些特殊的规则:

§13.5.6 [over.ref] p1

对于类型为T的类对象x,如果T::operator->()存在并且操作符被重载解析机制(13.3)选为最佳匹配函数,则表达式x->m被解释为(x.operator->())->m

上面的操作递归地应用,最后必须产生一个指针,该指针使用内置的operator->。这允许智能指针和迭代器的用户简单地执行smart->fun(),而不用担心任何事情。

规范Require:部分的注释:这些表示先决条件。如果您没有满足它们,则您正在调用UB。

那么为什么其中一个指定为noexcept而另一个指定为not呢?

老实说,我不确定。似乎对指针的解引用应该始终是noexcept,然而,unique_ptr允许您完全更改内部指针的类型(通过deleter)。现在,作为用户,您可以在您的pointer类型上为operator*定义完全不同的语义。也许它可以在飞行中进行计算?所有有趣的东西,这可能会扔。


看std::shared_ptr,我们有这个:

这很容易解释- shared_ptr不支持上述对指针类型的自定义,这意味着内置语义总是适用- *p (pT*)根本不抛出。

无论如何,这里有一点历史,以及事情是如何发展到现在的。

在N3025之前,operator *没有被指定为noexcept,但是它的描述中确实包含了一个Throws: nothing。在N3025:

中删除了此要求:

按指示(834)更改[unique.ptr.single.observers][详细信息请参见备注部分]:

typename add_lvalue_reference<T>::type operator*() const;
1 -要求:get() != 0 nullptr .
2 -返回:*get().

这是"评论"的内容。上面提到的部分:

在回顾本文的过程中,如何正确指定operator*、operator[]和异构比较函数的操作语义成为了争议。(结构。/3没有明确说明return元素(在没有新的Equivalent to公式的情况下)是否指定了效果。此外,还不清楚这是否允许这样的返回表达式通过异常退出,如果另外提供了一个Throws:-Nothing元素(是否需要实现者捕获这些?)为了解决这个冲突,为这些操作删除了任何现有的Throws元素,这至少与[unique.ptr.special]和标准的其他部分一致。这样做的结果是,我们现在隐式地支持潜在的抛出比较函数,但不支持同构的==和!=,这可能有点令人惊讶。

同一篇论文还包含了编辑operator ->定义的建议,但它的内容如下:

pointer operator->() const;
4 -要求:get() != 0nullptr.
5 -返回:get().
6 - throw: nothing.
7 -注意:使用时通常要求T为完整类型。

就问题本身而言:它归结为操作符本身与使用操作符的表达式之间的基本区别。

当使用operator*时,操作符对指针解引用,这会抛出。

当您使用operator->时,操作符本身只是返回一个指针(不允许抛出)。然后在包含->的表达式中解引用该指针。指针解引用的任何异常都发生在周围的表达式中,而不是在操作符本身中。

坦率地说,这在我看来只是一个缺陷。从概念上讲,a->b应该总是等同于(*a)。B,即使a是智能指针,这也适用。但是如果*a不是noexcept,那么(*a)。B不是,因此a-> B也不应该是。

关于:

是不是说运算符->实际上并没有解引用任何东西?

不,->对类型重载operator->的标准求值是:

a->b; // (a.operator->())->b

。计算是递归定义的,当源代码包含->时,应用operator->产生另一个表达式,其中->本身可以引用operator->

关于整个问题,如果指针为空,则行为未定义,并且缺少noexcept允许实现throw。如果签名是noexcept,则实现不能throw (throw将调用std::terminate)。