Why does std::unique_ptr operator* throw and opera

2020-02-08 23:38发布

In the C++ standard draft (N3485), it states the following:

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.

You can see that operator* (dereference) is not specified as noexcept, probably because it can cause a segfault, but then operator-> on the same object is specified as noexcept. The requirements for both are the same, however there is a difference in exception specification.

I have noticed they have different return types, one returns a pointer and the other a reference. Is that saying that operator-> doesn't actually dereference anything?

The fact of the matter is that using operator-> on a pointer of any kind which is NULL, will segfault (is UB). Why then, is one of these specified as noexcept and the other not?

I'm sure I've overlooked something.

EDIT:

Looking at std::shared_ptr we have this:

20.7.2.2.5 shared_ptr observers [util.smartptr.shared.obs]

T& operator*() const noexcept;

T* operator->() const noexcept;

It's not the same? Does that have anything to do with the different ownership semantics?

4条回答
forever°为你锁心
2楼-- · 2020-02-08 23:52

Regarding:

Is that saying that operator-> doesn't actually dereference anything?

No, the standard evaluation of -> for a type overloading operator-> is:

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

I.e. the evaluation is defined recursively, when the source code contains a ->, operator-> is applied yielding another expression with an -> that can itself refer to a operator->...

Regarding the overall question, if the pointer is null, the behavior is undefined, and the lack of noexcept allows an implementation to throw. If the signature was noexcept then the implementation could not throw (a throw would be a call to std::terminate).

查看更多
疯言疯语
3楼-- · 2020-02-08 23:59

For what it's worth, here's a little of the history, and how things got the way they are now.

Before N3025, operator * wasn't specified with noexcept, but its description did contain a Throws: nothing. This requirement was removed in N3025:

Change [unique.ptr.single.observers] as indicated (834) [For details see the Remarks section]:

typename add_lvalue_reference<T>::type operator*() const;
1 - Requires: get() !=0nullptr.
2 - Returns: *get().
3 - Throws: nothing.

Here's the content of the "Remarks" section noted above:

During reviews of this paper it became controversial how to properly specify the operational semantics of operator*, operator[], and the heterogenous comparison functions. [structure.specifications]/3 doesn't clearly say whether a Returns element (in the absence of the new Equivalent to formula) specifies effects. Further-on it's unclear whether this would allow for such a return expression to exit via an exception, if additionally a Throws:-Nothing element is provided (would the implementor be required to catch those?). To resolve this conflict, any existing Throws element was removed for these operations, which is at least consistent with [unique.ptr.special] and other parts of the standard. The result of this is that we give now implicit support for potentially throwing comparison functions, but not for homogeneous == and !=, which might be a bit surprising.

The same paper also contains a recommendation for editing the definition of operator ->, but it reads as follows:

pointer operator->() const;
4 - Requires: get() !=0nullptr.
5 - Returns: get().
6 - Throws: nothing.
7 - Note: use typically requires that T be a complete type.

As far as the question itself goes: it comes down to a basic difference between the operator itself, and the expression in which the operator is used.

When you use operator*, the operator dereferences the pointer, which can throw.

When you use operator->, the operator itself just returns a pointer (which isn't allowed to throw). That pointer is then dereferenced in the expression that contained the ->. Any exception from dereferencing the pointer happens in the surrounding expression rather than in the operator itself.

查看更多
一夜七次
4楼-- · 2020-02-09 00:08

A segfault is outside of C++'s exception system. If you dereference a null pointer, you don't get any kind of exception thrown (well, atleast if you comply with the Require: clause; see below for details).

For operator->, it's typically implemented as simply return m_ptr; (or return get(); for unique_ptr). As you can see, the operator itself can't throw - it just returns the pointer. No dereferencing, no nothing. The language has some special rules for p->identifier:

§13.5.6 [over.ref] p1

An expression x->m is interpreted as (x.operator->())->m for a class object x of type T if T::operator->() exists and if the operator is selected as the best match function by the overload resolution mechanism (13.3).

The above applies recursively and in the end must yield a pointer, for which the built-in operator-> is used. This allows users of smart pointers and iterators to simply do smart->fun() without worrying about anything.

A note for the Require: parts of the specification: These denote preconditions. If you don't meet them, you're invoking UB.

Why then, is one of these specified as noexcept and the other not?

To be honest, I'm not sure. It would seem that dereferencing a pointer should always be noexcept, however, unique_ptr allows you to completely change what the internal pointer type is (through the deleter). Now, as the user, you can define entirely different semantics for operator* on your pointer type. Maybe it computes things on the fly? All that fun stuff, which may throw.


Looking at std::shared_ptr we have this:

This is easy to explain - shared_ptr doesn't support the above-mentioned customization to the pointer type, which means the built-in semantics always apply - and *p where p is T* simply doesn't throw.

查看更多
够拽才男人
5楼-- · 2020-02-09 00:15

Frankly, this just looks like a defect to me. Conceptually, a->b should always be equivalent to (*a).b, and this applies even if a is a smart pointer. But if *a isn't noexcept, then (*a).b isn't, and therefore a->b shouldn't be.

查看更多
登录 后发表回答