35

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

20.7.1.2.4 unique_ptr 观察者 [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*(dereference) 没有被指定为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;

这是不一样的?这与不同的所有权语义有什么关系吗?

4

4 回答 4

28

段错误在 C++ 的异常系统之外。如果取消引用空指针,则不会引发任何类型的异常(好吧,至少如果您遵守该Require:子句;有关详细信息,请参见下文)。

For operator->,它通常被简单地实现return m_ptr;(或return get();for unique_ptr)。如您所见,运算符本身不能抛出 - 它只是返回指针。没有取消引用,什么都没有。该语言有一些特殊的规则p->identifier

§13.5.6 [over.ref] p1

如果存在并且运算符被重载决策机制(13.3)选择为最佳匹配函数,则表达式x->m被解释为类型(x.operator->())->m的类对象。xTT::operator->()

以上递归适用,最后必须产生一个指针,使用内置指针operator->。这使得智能指针和迭代器的用户可以简单地做smart->fun()而不必担心任何事情。

Require:规范部分的注释:这些表示先决条件。如果你没有遇到他们,你就是在调用 UB。

那么,为什么其中一个被指定为 noexcept 而另一个不是呢?

老实说,我不确定。似乎取消引用指针应该始终是noexcept,但是,unique_ptr允许您完全更改内部指针类型是什么(通过删除器)。现在,作为用户,您可以为operator*您的pointer类型定义完全不同的语义。也许它会即时计算事物?所有有趣的东西,可能会抛出。


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

这很容易解释 -shared_ptr不支持上述对指针类型的自定义,这意味着内置语义始终适用 - 并且*pwhere pisT*根本不会抛出。

于 2013-03-04T14:47:42.833 回答
4

对于它的价值,这里有一点历史,以及事情是如何发展到现在的。

在 N3025 之前,operator *没有用 指定noexcept,但它的描述确实包含Throws: nothing. N3025中删除了此要求:

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

typename add_lvalue_reference<T>::type operator*() const;
1 - 需要:get() != 0nullptr
2 - 返回:*get().
3 - 抛出:没有。

以下是上述“备注”部分的内容:

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

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

pointer operator->() const;
4 - 要求:get() != 0 nullptr。
5 - 返回:get()。
6 - 抛出:什么都没有。
7 - 注意:使用通常要求 T 是一个完整的类型。

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

使用operator*时,运算符会取消引用指针,该指针可能会抛出。

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

于 2014-02-09T01:02:32.570 回答
1

坦率地说,这对我来说只是一个缺陷。从概念上讲,a->b 应该始终等价于 (*a).b,即使 a 是智能指针也是如此。但是如果 *a 不是 noexcept,则 (*a).b 不是,因此 a->b 不应该是。

于 2013-03-04T19:07:35.920 回答
-2

关于:

那是说 operator-> 实际上并没有取消引用任何东西吗?

->不,类型重载的标准评估operator->是:

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

即评估是递归定义的,当源代码包含 a->时,operator->应用产生另一个表达式,其->本身可以引用 a operator->...

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

于 2013-03-04T14:47:16.573 回答