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

4

35

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?

Landed answered 4/3, 2013 at 14:14 Comment(2)
"Is that saying that operator-> doesn't actually dereference anything?" Yes, that's itPanel
Maybe it's to allow implementations to throw on a null pointer dereference if they so choose.Interaction
S
29

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.

Salts answered 4/3, 2013 at 14:47 Comment(9)
You can change the internal pointer type, but I don't think it's because of the deleter. The pointer is "std::remove_reference<D>::type::pointer if that type exists, otherwise T*"Boaster
You're probably right about the possibility of operator* overload, though.Boaster
@Stephen: D is the deleter type, and if that defines a nested pointer type, that one is used.Salts
@Xeo, oh, right, my mistake, thought it was T instead of D thereBoaster
Thanks, this let me realize that the deleter in unique_ptr may do much more than just delete!Carton
shared_ptr also contains a deleter so I don't know why unique_ptr is unique (pun intended ;)) in this regard.Stocky
@Red: It may contain a deleter, but that deleter has no influence whatsoever over the pointer type. Join me in the Lounge<C++> chatroom for more info.Salts
One use case for throwing on unique_ptr dereference is a custom pointer type that throws on null dereference.Chamaeleon
I'd like to link this code as a follow-up for the custom deleter versionTuttle
P
4

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.

Prothalamion answered 9/2, 2014 at 1:2 Comment(0)
E
1

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.

Euphonize answered 4/3, 2013 at 19:7 Comment(1)
open-std.org/jtc1/sc22/wg21/docs/lwg-active.html#2337 is related. It's likely to be closed as NAD, which wouldn't make the unique_ptr members consistent.Q
W
-2

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).

Week answered 4/3, 2013 at 14:47 Comment(7)
So what is the type of (a.operator->()) ?Landed
Having the behaviour undefined already allows the implementation to throw, even if the function is noexcept.Beacon
@TonyTheLion: Whatever you define it to be... it can yield a pointer or an object, if it yields an object, operator-> will be called on that so that a->b is translated into ((a.operator->())->operator->())b, unless the second evaluation also yields a non-pointer in which case...Duggan
@R.MartinhoFernandes: No, not really. The contract requires that it does not throw, and the compiler will generate code that traps the exception and calls std::terminate(). The result of calling a function that is nothrow will never be an exception.Duggan
@David undefined behaviour means that the standard places no requirements on the behaviour. That means it can very well throw on null dereference, regardless of noexcept. The contract requires that it does not throw but it also requires that you do not apply it to null pointers. You can only expect the contract to be met if you comply with it.Beacon
@R.MartinhoFernandes: I don't think you understand my point... when the compiler processes a nothrow function it adds code that on the presence of an exception will call std::terminate. Yes, the implementor of std::unique_ptr<> can decide to add throw 1;, and it would be as valid as any other undefined behavior you can think of, but the compiler generated code will catch that and transform it into a call to std::terminate. There is no flag when throwing that says BTW, I am in UB-land, don't call std::terminate and let the exception through...Duggan
@R.MartinhoFernandes: I am not saying that the implementation is not allowed to throw, I am saying that it would be absurd in a function marked as nothrow, as the compiler will turn it into std::terminate() (so why bother throwing? just call std::terminate() directly!)Duggan

© 2022 - 2024 — McMap. All rights reserved.