Default argument vs overloads in C++
For example, instead of
void shared_ptr::reset() noexcept; template <typename Y> void shared_ptr::reset(Y* ptr);
one may think of
template <typename Y = T> void shared_ptr::reset(Y* ptr = nullptr);
I think performance difference is negligible here, and the second version is more concise. Is there any specific reason the C++ standard goes the first way?
The same question has been asked for the Kotlin language, and default argument is preferred there.
If you are OFTEN resetting to precisely
nullptrrather than a new value, then the separate function
void shared_ptr::reset() noexcept;will have a space advantage, since you can use the one function for all types
Y, rather than have a specific function that takes a
Ytype for every type of
Y. A further space advantage is that the implementation without an argument doesn't need an argument passed into the function.
Of course, neither matters much if the function is called many times.
There is also difference in the exception behaviour, which can be highly important, and I believe this is the motiviation as to why the standard has multiple declarations of this function.
The crucial difference, is that the two operations are in fact not semantically the same.
The first is meant the leave the
shared_ptrwithout 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 ifmitigate 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.
There is a fundamental difference between an overload and a default pointer:
- the overload is self contained: the code in the library is completely independent of the calling context.
- the default parameter is not self contained but depend on the declaration used in the calling context. It can be redefined in a given scope with a simple declaration (e.g. a different default value, or no default value anymore.
So semantically speaking, the default value is a short-cut embeded in the calling code, whereas the overload is a meaning embedded in the called code.
While the design choices of the other answers are all valid, they do assume one thing that does not fully apply here: Semantic equivalence!
void shared_ptr::reset() noexcept; // ^^^^^^^^ template <typename Y> void shared_ptr::reset(Y* ptr);
The first overload is
noexcept, while the second overload isn't. There is no way to decide the
noexcept-ness based on the runtime value of the argument, so the different overloads are needed.
Some background information about the reason for the different
reset()does not throw since it is assumed that the destructor of the previously contained object does not throw. But the second overload might additionally need to allocate a new control block for the shared pointer state, which will throw
std::bad_allocif the allocation fails. (And
resetting to a
nullptrcan be done without allocating a control block.)