Default Parameters in C++: The Facts (Including Secret Ones)

Even though default parameters are simple to understand for their basic usage, there are many things to know to make the most out of them. Like, really many.

To explore various aspects of this vast topic, let’s start a series of post dedicated to the topic:

How default parameters relate to expressiveness

I’m convinced that default parameters can be a powerful tool to make our code more expressive.

The thing is that default parameters allow to hide things. They hide an extra argument from a call site. Consider this function for example:

void f(int x, double d = 42.5); 1 void f ( int x , double d = 42.5 ) ;

The caller of f doesn’t have to care about d . This make for more concise code at call site, and for less info to process for a reader of the code. Also, the call site doesn’t bear the responsibility of passing the correct default value. If the default value of the function changes, the call site only has to recompile to use the new default.

But is hiding arguments at call site always a good idea? Not necessarily. There is a slim line between hiding useless details and concealing valuable information. How to choose between hiding a value behind a default parameter or forcing its caller to pass it explicitly? Often, it comes down to respecting levels of abstraction. We’ll see concrete examples of that in the later posts of the series.

Anyway, mastering default parameters helps making rational decisions when using them and also avoiding pitfalls.

Too much of default parameters give way to implicit conversions

Here is one pitfall to avoid. Consider the following class:

class A { public: A(int i); // ... }; 1 2 3 4 5 6 class A { public : A ( int i ) ; // ... } ;

This class is contructible with an int . But even more than that, it is also implicitly convertible from an int . Implicit conversions are generally frowned upon, because they make the code a bit too implicit for a human being to follow. For this reason, we pretty much always add the explicit keyword in such a case:

class A { public: explicit A(int i); // ... }; 1 2 3 4 5 6 class A { public : explicit A ( int i ) ; // ... } ;

Fine. But now consider the following code:

class A { public: A(int i, double d = 4.5); // ... }; 1 2 3 4 5 6 class A { public : A ( int i , double d = 4.5 ) ; // ... } ;

A is still implicitly convertible from an int ! Indeed, A is convertible from an int as soon as its constructor can be called with an int . The fact that the second argument is optional allows the constructor to be called with an int . So we still need to mark this constructor explicit .

The same goes for the following class, whose constructor only has default parameters:

class A { public: A(int i = 3, double d = 4.5); // ... }; 1 2 3 4 5 6 class A { public : A ( int i = 3 , double d = 4.5 ) ; // ... } ;

The constructor can be called with an int , so it is implicitly convertible from an int until we mark it explicit . Which we should do.

Default values can have sophisticated constructions

The examples above use simple literal for default values: 3 , or 4.5 . But we can also initialize default values with a constructor. And this constructor can even take arguments:

class Widget { public: Widget(int i); // ... }; const int myValue = 42; void f(Widget const& w = Widget(myValue)); 1 2 3 4 5 6 7 8 9 10 class Widget { public : Widget ( int i ) ; // ... } ; const int myValue = 42 ; void f ( Widget const & w = Widget ( myValue ) ) ;

The cost of this is to make the definition of the class visible from the function declaration.

You can also initialize the default parameters with the result of a function:

Widget createWidget(); void f(Widget const& w = createWidget()); 1 2 3 Widget createWidget ( ) ; void f ( Widget const & w = createWidget ( ) ) ;

The thing that you can’t do though, is using an argument in the default value of another argument of the function, like so:

void f(int x, int y = x); 1 void f ( int x , int y = x ) ;

The order of evaluation of the arguments in left to the discretion of the compiler so there is no guarantee that x will be evaluated before y anyway. If you need to achieve this you can use two overloads instead:

void f(int x, int y) { ... } void f(int x) { f(x, x); } 1 2 3 4 5 6 7 8 9 void f ( int x , int y ) { . . . } void f ( int x ) { f ( x , x ) ; }

But more on default parameters versus overloads in the next post of the series.

The constraints of default parameters

Default parameters have two constraints that can hinder expressiveness: their position and their interdependence.

All the default parameters have to be at the end of the arguments list of a function. This can make an interface less natural, because arguments are no longer grouped in a logical order. Instead, they are grouped in a technical order: the non-default parameters first, then the default ones. This can be confusing at call site.

The second constraint is their interdependence: if there are several default parameters , and a call site wants to pass a value for one of them, then it has to also provide a value for all the other default parameters preceding it in the arguments list of the function. This again makes for bizarre call sites.

The Defaulted helper presented in a later post of the series, aims at working around these two constraints.

Local defaults: A secret feature of default parameters

Finally, here is an fairly uncommon functionality of default parameters. Even if a function does not have default parameters in its interface, you can use it just as if it had some.

To do that you can redeclare the function in the scope you want to use, this time with a default parameter.

Here is an example. Consider this function sum that does not have default parameters:

int sum(int x, int y) { return x + y; } 1 2 3 4 int sum ( int x , int y ) { return x + y ; }

And here is another function, f , that uses sum , but say that we would like a default value for sum ‘s second argument in all the scope of f .

We can then redeclare sum in the scope of f with a default parameter, and use it like so:

void f() { int sum(int x, int y = 5); // redeclaration of sum with default parameter std::cout << sum(10) << '

'; // usage that relies on the default value std::cout << sum(20) << '

'; // same thing } int main() { f(); } 1 2 3 4 5 6 7 8 9 10 11 12 void f ( ) { int sum ( int x , int y = 5 ) ; // redeclaration of sum with default parameter std :: cout << sum ( 10 ) << '

' ; // usage that relies on the default value std :: cout << sum ( 20 ) << '

' ; // same thing } int main ( ) { f ( ) ; }

And the following code outputs this:

15 25 1 2 15 25

If you’d like to see more secret and crazy features of default parameters, have a look at this CppCon talk where Michael Price spends an hour talking about default parameters and shows mind-bending situations using them.

Over to you

How do you use default parameters in your code? Are you happy with them?

Sharing knowledge helps getting better all together, so let us know the interesting things you have achieved with default parameters!

You may also like

Share this post! Don't want to miss out ?