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?
Regarding:
No, the standard evaluation of
->
for a type overloadingoperator->
is: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 aoperator->
...Regarding the overall question, if the pointer is null, the behavior is undefined, and the lack of
noexcept
allows an implementation tothrow
. If the signature wasnoexcept
then the implementation could notthrow
(athrow
would be a call tostd::terminate
).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 withnoexcept
, but its description did contain aThrows: nothing
. This requirement was removed in N3025:Here's the content of the "Remarks" section noted above:
The same paper also contains a recommendation for editing the definition of
operator ->
, but it reads as follows: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.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 simplyreturn m_ptr;
(orreturn get();
forunique_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 forp->identifier
:§13.5.6 [over.ref] p1
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 dosmart->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.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 foroperator*
on yourpointer
type. Maybe it computes things on the fly? All that fun stuff, which may throw.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
wherep
isT*
simply doesn't throw.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.