#090 – How to implement optional parameters in C++ using default arguments

A default argument is a fallback value used when the caller does not provide an argument, therefore it plays the role of an optional parameter.

The Premise

A default argument is created using =

In the example below, if the caller chooses not to supply an argument, the default argument is chosen, otherwise the caller’s argument takes precedence.

#include <iostream>

void connectToServer(int timeoutSeconds = 30)
{
    std::cout << "Connecting with timeout: "
              << timeoutSeconds << " seconds\n";
}

int main()
{
    connectToServer();     // uses default timeout
    connectToServer(10);   // overrides default timeout
}

// terminal
Connecting with timeout: 30 seconds
Connecting with timeout: 10 seconds

When default arguments are useful

Default arguments are perfect for when a function requires a standard value which can be overridden at the caller’s discretion. For example:

int rollDie(int sides = 6);

Most dice are six-sided, so 6 is a sensible default. However, a caller can request a different die:

rollDie();   // six-sided die
rollDie(20); // twenty-sided die

Also, it’s perfect for updating a function without breaking existing function calls. This is done by expanding the parameter list. Consider the following:

#include <iostream>
#include <string>

void saveFile(const std::string& filename, bool compress = false)
{
    std::cout << "Saving " << filename;

    if (compress) {  std::cout << " with compression enabled"; }
    std::cout << '\n';
}

int main()
{
    saveFile("report.txt");
        // Existing function call still works

    saveFile("report.txt", true);
        // New functionality available when needed
}

Suppose this function originally only accepted a filename:

void saveFile(const std::string& filename);

Later, compression support is added. By introducing a default argument, older code continues compiling unchanged, while newer code can opt into the new behaviour.

Multiple default arguments

A function can have default arguments with multiple parameters and even consisting of different types. In the example below, the same function is called in 4 unique ways.

#include <iostream>
#include <string>

void createWindow(int width = 1280, int height = 720,  std::string title = "Untitled")
{
    std::cout << "Width: "  << width  << '\n'
              << "Height: " << height << '\n'
              << "Title: "  << title  << "\n\n";
}

int main()
{
    createWindow();
    createWindow(1920);
    createWindow(1920, 1080);
    createWindow(1920, 1080, "Tic Tac Toe");
}

// Output
Width: 1280
Height: 720
Title: Untitled

Width: 1920
Height: 720
Title: Untitled

Width: 1920
Height: 1080
Title: Untitled

Width: 1920
Height: 1080
Title: Raylib Game

NOTE: You can’t do (as of C++23) createWindow(2200, "Snake") i.e. override the last 2 parameters. Therefore, the parameters are overridden in order of the arguments.

You cannot skip leftmost arguments

You must pass in the arguments in the order in which they appear in the parameter list. Arguments are matched left to right.

In the example below, for the second function call, 5 is used as the first parameter. Since there is no match and the compiler cannot use numeric conversions to convert an int into std::string, it fails.

#include <iostream>
#include <string>

void launchMissile(std::string target = "Training Dummy", int countdownSeconds = 10)
{
    std::cout << "Target: " << target << " | Countdown: "
              << countdownSeconds << " seconds\n";
}

int main()
{
    launchMissile("Enemy Base");
        // okay: countdownSeconds defaults to 10

    launchMissile(5);
        // ERROR
}

Defaults go on the right

If a parameter is given a default type, all parameters to the right of it must also have a default argument. Otherwise the compiler would be unable to resolve a function call. Consider the following:

int add(int x = 10, int y) 

add(5);

Does 5 initialise x, leaving y missing? Or should x use its default and 5 initialise y?

C++ avoids that ambiguity by requiring defaults to appear from right to left:

int add(int x, int y = 10); // OK

Now this is clear:

add(5); // x = 5, y = 10

Default arguments must be populated in the forward declaration

Default arguments are used by the compiler at the call site.

That means the default must be visible before the function is called.

If the default argument is defined after the functional call, the compiler will throw an error even if a forward declaration is used. See the program below:

#include <iostream>

void print(int x, int y); // forward declaration, no default argument

int main()
{
    print(3); // compile error: default argument for y hasn't been defined yet
}

void print(int x, int y=4) // Function implementation
{
    std::cout << "x: " << x << '\n';
    std::cout << "y: " << y << '\n';
}

/tmp/Fg3md3gO6B/main.cpp:7:10: error: too few arguments to function 'void print(int, int)’

The solution is to populate the parameter list in the forward declaration with the default argument.

#include <iostream>

void print(int x, int y = 13); // forward declaration with default argument

int main()
{
    print(3); 
}

void print(int x, int y) // Function implementation
{
    std::cout << "x: " << x << '\n';
    std::cout << "y: " << y << '\n';
}

Default arguments and function overloading

Since default arguments alter the function signature, they can be used to easily implement overloaded functions.

void print(int x);                  
void print(int x, int y = 10);       
void print(int x, double y = 20.5);  

However, this should be used with caution as it can easily create ambiguous function calls. For example, considering the following what would print(11); resolve to? The compiler has no preference.

/tmp/zdAEihbKUw/main.cpp:8:10: error: call of overloaded 'print(int)' is ambiguous
    8 |     print(11);

Takeaway

Default arguments are used when the caller omits the arguments.

They are useful when a parameter has a natural base value that can be overridden at the caller’s discretion, or when expand functions without tampering with existing function calls.