Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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.

Update:

std::unique_ptr::reset() follows the default argument design (see here). So I think the reason std::shared_ptr::reset() uses overloads is because they have different exception specifications.

like image 544
Lingxi Avatar asked Feb 18 '18 08:02

Lingxi


People also ask

What are the advantages of default arguments over function overloading?

Using default arguments reduces the number of functions you need to find, because it reduces the number of overloads. It makes sure that as time goes by, the actual logic of the two overloads with different numbers of arguments don't drift apart, intentionally or accidentally.

What is default argument in function overloading?

A default argument is a value provided in a function declaration that is automatically assigned by the compiler if the calling function doesn't provide a value for the argument. In case any value is passed, the default value is overridden.

Can we overloaded functions with default argument?

No you cannot overload functions on basis of value of the argument being passed, So overloading on the basis of value of default argument is not allowed either. You can only overload functions only on the basis of: Type of arguments. Number of arguments.

When should default arguments be preferred over function overloading and vice versa?

A default parameter can only supply a value of the same type as a parameter. An overloaded function can use completely different parameters. That's just one difference. Sometimes you want to use different parameter types for different overloads.


2 Answers

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.

like image 55
StoryTeller - Unslander Monica Avatar answered Oct 06 '22 00:10

StoryTeller - Unslander Monica


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.

like image 39
Mats Petersson Avatar answered Oct 06 '22 00:10

Mats Petersson