Cost of Default parameters in C++
Asked Answered
R

3

49

I stumbled through an example from "Effective C++ in an Embedded Environment" by Scott Meyers where two ways of using default parameters were described: one which was described as costly and the other as a better option.

I am missing the explanation of why the first option might be more costly compared to the other one.

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

const std::string defaultName = "Unnamed";
void doThat(const std::string& name = defaultName); // Better
Rainproof answered 20/2, 2018 at 8:45 Comment(12)
It could be that for the "bad" example, the code have to create a std::string object for the default argument on every call. It's not needed for the "better" example.Embouchure
@Someprogrammerdude - I'd agree with your use of quotations, if not for this being about "Embedded Environments". Sure, one better measure performance. But Scott Meyers usually doesn't give advice unless there's some merit to it.Humeral
You'd have to check what actual compilers do, though. And note that there is a 3rd form which isn't mentioned: void doThat(const std::string& name = "Unnamed"s ); <= Trailing 's' makes it a true string literal.Toner
Really? You "missed the explanation"? It literally says "Note that they are always passed. Poor design can thus be costly:" on the same slide you're referring to.Wallace
@Toner will it reduce the cost by making it string literal?Ipomoea
@army007: Yes; that is essentially equivalent to the first form, but without a name for the object.Toner
@Toner - Is it really? "A default argument is evaluated each time the function is called with no argument for the corresponding parameter.". That means that string literal is formally evaluated and materialized at each call. I'm sure it can, and probably is optimized. But it's not a 1-to-1 equivalence with the named version.Humeral
@StoryTeller: The expresson defaultName 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 to std::string::string(const char*) which includes a call to strlen.Toner
@Toner - Yes, but the expression defaultName is the name of a pre-created object. "whatever"s is formally a function call too.Humeral
@Toner Even that call to strlen may or may not happen, especially if doThat can be inlined. godbolt.org/g/n1tpbESall
@Toner I'd be really surprised if embedded embedded environments supported C++14 in 2015 (when book was written). I wouldn't be surprised if they hadn't had full C++11 support now so I wouldn't count on C++14 features.Leboff
@MaciejPiechotka Depends on how you define "embedded". Here you can see a game written in C++17 running on a C64.Sall
G
57

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.

Girl answered 20/2, 2018 at 8:48 Comment(5)
So now we have a string construction "once per source file" rather than "once per call". If the default argument isn't used that often, this might actually result in an increased code size. For example, if you include the header to call some other function you might get an extra string constructed for no good reason. Scott's advice now looks more like "something to consider" rather than generally "better".Mushro
Nice...does this aply to c# also?Trumaine
@BoPersson Since defaultName has internal linkage, I would assume the compiler to be smart enough to eliminate it altogether if it's never used by the file.Girl
@MarioGarcia No idea, I never wrote a single line of C# code.Girl
@MarioGarcia: C# requires that default parameters be compile-time constants. This example would work because C# string literals are constants, but you couldn't do it with a more complicated type, or if you wanted to use the string constructor that took a character array. This will produce a compiler error, so there won't be any runtime cost.Hereby
O
19
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 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, 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).

Omentum answered 20/2, 2018 at 18:25 Comment(5)
"every time doThat is called" without arguments. Right?Anchie
@LightnessRacesinOrbit () added.Omentum
Ok - was honestly checking for my own peace of mind as well as for editorial reasons. It gave me a bit of a scare..Anchie
@LightnessRacesinOrbit What, you missed the speculative default argument evaluation part of the C++ standard? You know, just in case you need it in a different timeline. Good old [spec.arg.eval] ;) (Warning: this comment contains humour, do not take it seriously)Omentum
"Alexa, find the nearest bridge please..."Anchie
C
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
Catbird answered 20/2, 2018 at 9:5 Comment(5)
Sorry for nitpicking, I can't help but feel that using the foo_default as default for bar() (as well as for foo()) is somewhat counter intuitive (principle of least surprise etc.). foo_and_bar_default might be a better name (or one might want to consider refactoring the entire thing)Lactose
@Lactose I dont really agree, but maybe my example isnt the best. For the user there is no surprise as in bar() the name of foo_default doesnt really matter to the client (other than providing a hint that eventually the parameter is passed to foo). I would rather add a const int bar_default = foo_default;. Anyhow, the point of my answer still holds: using values as defaults isnt nice and this can be fixed by using OPs approachCatbird
@Lactose btw no need to be sorry for nitpicking, actually I appreciate itCatbird
The best thing to do would probably decide on a name that's what the default actually represents -- NO_OPTION, NOT_MOD, whatever -- and use the same variable for both. That way it's explicitly clear. It's hard to come up with a good example for this, since by definition foo and bar don't... actually do anything. It would be worth mentioning that in the prose, though. Or maybe renaming foo_default to default_value?Boo
My experience is, that it is usually not a good idea to use default arguments in the first place. Simply because default arguments work on the caller side. I find that it's usually much better to just overload the function (possibly with a call-through to a canonical variant), or to just provide the correct default at every call. The former makes the defaulting an implementation detail that is none of the callers business, and the later is explicit about what constant is passed at each call. Also, defaults should never be anything other than 0 or nullptr anyway. Just my 2ct.Mcdermott

© 2022 - 2024 — McMap. All rights reserved.