Skip to content
C++

Default Parameters

Most functions are called with the same arguments every time. Default parameter values let you define what a function does when the caller does not provide a particular argument, reducing the number of overloads you need to write and making call sites shorter and cleaner.

What default parameter values are

A default argument value is a value you assign to a parameter in a function declaration. When the caller omits that argument, the compiler substitutes the default automatically. If the caller supplies a value, the default is ignored and the supplied value is used instead.

Consider a function that outputs an error message. Most of the time the caller wants to print the generic message “Program Error”; only occasionally does the caller need something different. Without defaults, you would either pass the string every single time or write two separate overloads. With a default, one declaration covers both:

// Declaration with a default argument value:
void print_error(const std::string& message = "Program Error");

// Definition (body) — no default repeated here:
void print_error(const std::string& message) {
    std::println("{}", message);
}

int main() {
    print_error();                   // uses default → prints "Program Error"
    print_error("Nothing works!");   // overrides default → prints "Nothing works!"
}

The default is specified in the declaration (prototype), not in the function definition. This is because the compiler needs to see the default when it compiles the call site — it must know whether a given number of arguments is acceptable. When declaration and definition are in the same place (no separate header), you can put the default on the definition directly, but you cannot repeat it in both.

The trailing rule — defaults must be at the end

C++ requires that all parameters with default values be grouped at the end of the parameter list. This is a hard rule: once you give a parameter a default, every parameter that comes after it in the list must also have a default. The reason is that when you omit a trailing argument, all arguments after it must be omitted too — you cannot skip a parameter in the middle of a call.

// Valid — defaults are at the trailing end:
void print_data(const int data[], std::size_t count,
                const std::string& title = "Data Values",
                unsigned width           = 10,
                unsigned per_line        = 5);

// Calling with fewer and fewer arguments (omit from the right):
print_data(samples, 12);                              // title, width, per_line are defaults
print_data(samples, 12, "Samples");                   // width and per_line are defaults
print_data(samples, 12, "Samples", 6);                // per_line is default
print_data(samples, 12, "Samples", 6, 4);             // no defaults used

// You cannot skip an argument in the middle:
// print_data(samples, 12, , 8, 4);   // ERROR — cannot omit title but supply width

The declaration below is invalid because count has a default but the parameter before it (data) does not, while a parameter after it (title) also lacks a default — breaking the “defaults trail” rule:

// INVALID — title comes after count but has no default,
// so count cannot have a default either:
void print_data(const int data[], std::size_t count = 1,
                const std::string& title);            // ERROR

Default values and reference parameters

Default values work naturally with pass-by-value parameters and with reference-to-const parameters. However, you cannot supply a default value for a non-const reference parameter. The reason involves implicit conversions: when you call print_error() with no argument, the compiler creates a temporary std::string object from the string literal and passes a reference to it. Temporaries can only bind to const references, not to modifiable ones.

// Pass-by-value default — always works:
void print_error(std::string message = "Program Error");

// Reference-to-const default — preferred (avoids copying the string):
void print_error(const std::string& message = "Program Error");   // Best

// Non-const reference default — NOT allowed, will not compile:
// void print_error(std::string& message = "Program Error");      // ERROR

The reference-to-const form is generally preferred: it avoids constructing an unnecessary copy of the string when the caller does provide an argument, while still allowing string literals and temporary strings to be used as defaults.

Choosing good default values

Not every parameter needs — or benefits from — a default. The best defaults are predictable, sensible, and safe: a caller who does not bother to read the documentation should get behaviour that makes obvious sense. A formatting function that prints a number in the most natural way by default is good; one that defaults to a hex representation of a floating-point exponent is not.

// Good default — {} format is exactly what you expect when you see println(1.23):
void println(double value, const std::string& format = "{}");

// Poor default — few callers will want hex floating-point output by default:
void println(double value, const std::string& format = "{:a}");   // confusing

// Also poor — 6 decimal places sounds specific but is rarely what either
// human output or precision-preserving serialisation needs:
void println(double value, const std::string& format = "{:.6f}"); // often wrong

Another rule of thumb: if most callers end up overriding the default anyway, the default is not actually reducing work — it is hiding a choice that callers should be making explicitly. In that case, remove the default and force callers to be explicit.

Default parameters should also be ordered from least likely to be omitted (first among the defaults) to most likely to be omitted (last). This maximises how often the trailing-omit rule is useful in practice.

Default parameters vs. function overloading

Default parameters and function overloading solve similar problems — allowing a function to be called in multiple ways — but they are different mechanisms. Default parameters are simpler when the missing argument has a single sensible stand-in value. Overloading is better when the variant that handles the missing argument needs fundamentally different logic:

// Overload approach — useful when "no separator" means different logic:
void join(const std::vector<std::string>& words);                // joins with nothing
void join(const std::vector<std::string>& words, char separator);// joins with separator

// Default parameter approach — simple substitution is enough:
void join(const std::vector<std::string>& words, char separator = ' ');

One thing to be careful about: combining default arguments with overloading can make calls ambiguous. If two overloads both match a given call after defaults are applied, the compiler will reject the call as ambiguous. Keep your overload sets simple and test calls that might be borderline.

void display(int n, bool hex = false);
void display(int n);                        // ERROR: ambiguous with above

display(42);       // which overload? both match — compiler error

Key rules to remember

Default values go in the declaration, not in the definition

The compiler needs to see defaults at the call site, which is where the declaration is visible. If you repeat a default in both, the compiler will reject it as a redeclaration.

All defaulted parameters must be at the trailing end

Once a parameter has a default, every parameter after it must also have a default. Callers omit from the right — they cannot skip parameters in the middle.

Non-const reference parameters cannot have default values

Defaults for reference parameters require creating a temporary, and temporaries can only bind to const references. Use const std::string& message = "default" not std::string& message = "default".

Order defaults from least-likely to most-likely to be omitted

This maximises the usefulness of the trailing-omit rule. Put the most 'optional' parameter last.

If most callers override the default, remove it

A default that is always overridden is not reducing noise — it is hiding an important decision. Explicit is better than implicit when the value actually matters.

Sign in to track progress