Cost of Default parameters in C++

Solution 1:

In the first one, a temporary std::string is initialised from the literal "Unnamed" each time the function is called without an argument.

In the second case, the object defaultName is initialised once (per source file), and simply used on each call.

Solution 2:

void doThat(const std::string& name = "Unnamed"); // Bad

This is "bad" in that a new std::string with the contents "Unnamed" is created every time doThat() is called.

I say "bad" and not bad because the small string optimization in every C++ compiler I've used will place the "Unnamed" data within the temporary std::string created at the call site and not allocate any storage for it. So in this specific case, there is little cost to the temporary argument. The standard does not require the small string optimization, but it is explicitly designed to permit it, and every standard library currently in use implements it.

A longer string would cause an allocation; the small string optimization works on short strings only. Allocations are expensive; if you use the rule of thumb that one allocation is 1000+ times more expensive than a usual instruction (multiple microseconds!), you won't be far off.

const std::string defaultName = "Unnamed";
void doThat(const std::string& name = defaultName); // Better

Here we create a global defaultName with the contents "Unnamed". This is created at static initialization time. There are some risks here; if doThat is called at static initialization or destruction time (before or after main runs), it could be invoked with an unconstructed defaultName or one that has already been destroyed.

On the other hand, there is no risk that a per-call memory allocation will occur here.


Now, the right solution in modern c++17 is:

void doThat(std::string_view name = "Unnamed"); // Best

which won't allocate even if the string is long; it won't even copy the string! On top of that, in 999/1000 cases this is a drop-in replacement to the old doThat API and it can even improve performance when you do pass data into doThat and not rely on the default argument.

At this point, c++17 support in the embedded may not be there, but in some cases it could be shortly. And string view is a large enough performance increase that there are a myriad of similar types already in the wild that do the same thing.

But the lesson still remains; don't do expensive operations in default arguments. And allocation can be expensive in some contexts (especially the embedded world).

Solution 3:

Maybe I misinterpret "costly" (for the "correct" interpretation see the other answer), but one thing to consider with default parameters is that they dont scale well in situations like that:

void foo(int x = 0);
void bar(int x = 0) { foo(x); }

This becomes an error prone nightmare once you add more nesting because the default value has to be repeated in several places (ie costly in the sense that one tiny change requires to change different places in the code). The best way to avoid that is like in your example:

const int foo_default = 0;
void foo(int x = foo_default);
void bar(int x = foo_default) { foo(x); } // no need to repeat the value here