The crucial difference, is that the two operations are in fact not semantically the same.
The first is meant the leave the shared_ptr
without a managed object. The second is meant to have the pointer manage another object. That's an important distinction. Implementing it in a single function would mean that we'll essentially have one function do two different operations.
Furthermore, each operation may have different constraints on the types in question. If we dump them into one function, then "both branches" will have to satisfy the same constraints, and that's needlessly restrictive. C++17 and constexpr if
mitigate it, but those functions were specified before that exited.
Ultimately, I think this design is in line with Scott Meyers' advice. If the default argument has you doing something semantically different, it should probably be another overload.
Okay, so to address your edit. Yes, the exception specifications are different. But like I alluded to previously, the reason they can be different, is that the functions are doing different things. The semantics of the reset
members require this:
void reset() noexcept;
Effects: Equivalent to shared_-ptr().swap(*this)
.
template<class Y> void reset(Y* p);
Effects: Equivalent to shared_-ptr(p).swap(*this)
.
Not a big newsflash there. Each function has the effect of constructing a new shared_ptr
with the given argument (or lack thereof), and swapping. So what do the shared_ptr
constructors do? According to a preceding section, they do this:
constexpr shared_ptr() noexcept;
Effects: Constructs an empty shared_-ptr object.
Postconditions: use_-count() == 0 && get() == nullptr
.
template<class Y> explicit shared_ptr(Y* p);
Postconditions: use_-count() == 1 && get() == p
.
Throws: bad_-alloc
, or an implementation-defined exception when a resource other than memory could not be obtained
Note the different post conditions on the pointer's use count. That means that the second overload needs to account for any internal bookkeeping. And very likely allocate storage for it. The two overloaded constructors do different things, and like I previously said, that's a strong hint to separate them into different functions. The fact one can get a stronger exception guarantee is further testament to the soundness of that design choice.
And finally, why does unique_ptr
have only one overload for both actions? Because the default value doesn't change the semantics. It just has to keep track of the new pointer value. The fact that value is null (either from the default argument or otherwise), doesn't change the function's behavior drastically. A single overload is therefore sound.