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->mis interpreted as(x.operator->())->mfor a class objectxof typeTifT::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.