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?
A unique_ptr can only be moved. This means that the ownership of the memory resource is transferred to another unique_ptr and the original unique_ptr no longer owns it.
std::unique_ptr is a smart pointer that owns and manages another object through a pointer and disposes of that object when the unique_ptr goes out of scope. The object is disposed of, using the associated deleter when either of the following happens: the managing unique_ptr object is destroyed.
A std::unique_ptr owns of the object it points to and no other smart pointers can point to it. When the std::unique_ptr goes out of scope, the object is deleted. This is useful when you are working with a temporary, dynamically-allocated resource that can get destroyed once out of scope.
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 objectx
of typeT
ifT::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.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With