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.

4 answers

  • answered 2018-02-18 08:56 Mats Petersson

    If you are OFTEN resetting to precisely nullptr rather 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 Y type 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.

  • answered 2018-02-18 09:25 StoryTeller

    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.

  • answered 2018-02-18 09:34 Christophe

    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.

  • answered 2018-02-18 17:34 anderas

    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 noexcept specifications: 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_alloc if the allocation fails. (And resetting to a nullptr can be done without allocating a control block.)