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).
std::string
object for the default argument on every call. It's not needed for the "better" example. – Embouchurevoid doThat(const std::string& name = "Unnamed"s );
<= Trailing 's' makes it a true string literal. – TonerdefaultName
would also be evaluated at every call. That's not necessarily costly. The problem with the bad form is that the evaluation in that case consists of a call tostd::string::string(const char*)
which includes a call tostrlen
. – TonerdefaultName
is the name of a pre-created object."whatever"s
is formally a function call too. – Humeralstrlen
may or may not happen, especially ifdoThat
can be inlined. godbolt.org/g/n1tpbE – Sall