C++ Default Arguments

A default argument is a value that C++ automatically supplies for a function parameter when the caller doesn’t provide one. Instead of writing several overloaded versions of the same function just to handle optional inputs, you give a parameter a default value once in the function’s declaration, and callers can omit that argument entirely. This makes function calls shorter, keeps APIs flexible, and reduces the number of nearly-identical overloads you have to maintain.

Overview: How Default Arguments Work

In C++, every parameter in a function’s parameter list can optionally be given a default value using = value after its name. When a function is called, the compiler compares the number of arguments supplied against the number of parameters. If fewer arguments are given than parameters exist, the compiler fills in the missing trailing parameters with their default values — substituted directly at the call site, not inside the function body.

This substitution happens entirely at compile time. The compiler looks at the function’s declaration (the prototype it has seen so far, which may live in a header file or earlier in the same file), determines which parameters were omitted, and inserts the corresponding default expressions into the generated call. This is why default arguments must be known at the call site: if the compiler hasn’t seen a declaration with defaults yet, it cannot fill anything in, and a normal "too few arguments" error occurs.

Default arguments are a declaration-time feature, not a runtime one — there’s no hidden branching or dynamic dispatch involved. The generated machine code for greet("Alice") is indistinguishable from code that explicitly wrote greet("Alice", "Hello"); the compiler simply inserted the literal "Hello" for you before generating the call.

Default arguments also interact with function overload resolution. If a call could match multiple overloads once defaults are applied, or if defaults create ambiguity between overloads, the compiler will reject the call. This is one of several rules that keep default arguments from silently causing confusing behavior.

Syntax

returnType functionName(paramType param1, paramType param2 = defaultValue2, paramType param3 = defaultValue3) {
    // function body
}
  • paramType param1 — a normal, required parameter with no default. Callers must always supply this argument.
  • = defaultValue2 — the default value used when the caller omits this argument. It can be a literal, a constant expression, or even a call to another function that is visible at that point.
  • Trailing rule — once one parameter has a default, every parameter after it (to its right) must also have a default. You cannot have a defaulted parameter followed by a non-defaulted one.
  • One definition per default — a given parameter’s default value may be specified only once across all declarations of the function visible in a translation unit (typically in the prototype, not repeated in the definition).

Examples

Example 1: A basic greeting function

#include <iostream>
#include <string>
using namespace std;

void greet(string name, string greeting = "Hello") {
    cout << greeting << ", " << name << "!" << endl;
}

int main() {
    greet("Alice");
    greet("Bob", "Good morning");
    return 0;
}

Output:

Hello, Alice!
Good morning, Bob!

The first call to greet supplies only name, so the compiler substitutes the default string "Hello" for greeting. The second call explicitly overrides that default with "Good morning". Note that name has no default, so it must always be supplied.

Example 2: Multiple parameters with a computed default

#include <iostream>
using namespace std;

double calculateArea(double length, double width = 0) {
    if (width == 0) {
        width = length; // treat it as a square when width is omitted
    }
    return length * width;
}

int main() {
    cout << "Area of square (5x5): " << calculateArea(5) << endl;
    cout << "Area of rectangle (5x3): " << calculateArea(5, 3) << endl;
    return 0;
}

Output:

Area of square (5x5): 25
Area of rectangle (5x3): 15

Here the default value 0 acts as a sentinel: the function body checks whether width was left at its default and, if so, reuses length to compute the area of a square. This is a common pattern when the "natural" default depends on another argument rather than being a fixed literal.

Example 3: Declaring defaults in a prototype, multiple trailing defaults

#include <iostream>
using namespace std;

int calculatePrice(int basePrice, double taxRate = 0.08, double discount = 0.0);

int main() {
    cout << "Price 1: " << calculatePrice(100) << endl;
    cout << "Price 2: " << calculatePrice(100, 0.05) << endl;
    cout << "Price 3: " << calculatePrice(100, 0.05, 10) << endl;
    return 0;
}

int calculatePrice(int basePrice, double taxRate, double discount) {
    double price = basePrice - discount;
    price += price * taxRate;
    return static_cast<int>(price);
}

Output:

Price 1: 108
Price 2: 105
Price 3: 94

The default values are written in the prototype, not repeated in the definition below main — this is the idiomatic pattern once code is split between a declaration and a definition. calculatePrice(100) uses both defaults (8% tax, no discount) and yields 108. calculatePrice(100, 0.05) overrides only the tax rate. calculatePrice(100, 0.05, 10) overrides both, producing 90 * 1.05 = 94.5, which static_cast<int> truncates to 94.

How It Works Step by Step (Under the Hood)

  1. The compiler parses the function’s declaration and records, for each parameter, whether a default expression was supplied and what it is.
  2. At every call site, the compiler counts the arguments you actually wrote and matches them left-to-right against the parameter list.
  3. For any trailing parameters you didn’t supply, the compiler pulls the recorded default expression and inserts it as if you had typed it yourself at that call site.
  4. Type checking, implicit conversions, and overload resolution all run against this "filled in" argument list, exactly as they would for an explicit call.
  5. The function itself is compiled once, as an ordinary function with a fixed number of parameters — it has no idea whether a caller supplied a value explicitly or received the default. There is no runtime flag or extra branch for "was this argument defaulted."

Because the substitution happens at the call site using whatever declaration is visible there, if a header declares different defaults than what you expect (or a default depends on a global that changes), the value used can be surprising. Keeping default values simple and declared in exactly one place avoids this entirely.

Common Mistakes

Mistake 1: Repeating the default in both the declaration and the definition

It’s tempting to write the default value again when you define the function, but a parameter’s default may be specified only once among all declarations seen in a translation unit. Repeating it is a compile error, even if the value is identical both times.

// Wrong: default repeated in the definition
int calculatePrice(int basePrice, double taxRate = 0.08);

int calculatePrice(int basePrice, double taxRate = 0.08) { // error: redefinition of default argument
    return static_cast<int>(basePrice * (1 + taxRate));
}

The fix is simple: keep the default only in the prototype and drop it from the definition, exactly as shown in Example 3.

int calculatePrice(int basePrice, double taxRate = 0.08);

int calculatePrice(int basePrice, double taxRate) { // correct: no default here
    return static_cast<int>(basePrice * (1 + taxRate));
}

Mistake 2: Putting a defaulted parameter before a required one

Default arguments must be trailing. Once you give one parameter a default, every parameter after it must have a default too — you can’t have a required parameter following a defaulted one, because the compiler fills in defaults strictly from right to left and there would be no unambiguous way to "skip over" a middle parameter.

// Wrong: 'unit' has a default but 'price' after it does not
void printTag(string unit = "USD", double price) { // error
    cout << price << " " << unit << endl;
}

Reorder so required parameters come first and defaulted ones trail at the end:

#include <iostream>
#include <string>
using namespace std;

void printTag(double price, string unit = "USD") {
    cout << price << " " << unit << endl;
}

int main() {
    printTag(19.99);
    printTag(19.99, "EUR");
    return 0;
}

Output:

19.99 USD
19.99 EUR

Best Practices

  • Put default arguments in the function’s declaration (prototype or header), not the definition, so anyone including the header sees the defaults without needing the implementation.
  • Only default parameters that have one obviously "normal" value; if a parameter’s sensible value varies a lot by call site, a default can hide bugs rather than prevent them.
  • Keep default expressions simple and side-effect free (literals or simple constants) so readers can predict behavior without reading other code.
  • Order parameters from most-important/required to least-important/optional, since defaults must trail.
  • Prefer default arguments over near-duplicate overloads when the only difference between overloads is a trailing optional value — it’s less code to maintain.
  • Watch for ambiguity with overloaded functions: if a call could match two different overloads once defaults are applied, the compiler will reject it, so avoid overload sets whose defaulted forms collide.

Practice Exercises

  1. Write a function power(double base, int exponent = 2) that returns base raised to exponent using a loop (no <cmath>). Call it once with only base and once with both arguments, and print both results.
  2. Write a function formatName(string first, string last, bool lastFirst = false) that returns "first last" normally, or "last, first" when lastFirst is true. Test all three ways of calling it (omitting the third argument, passing false, and passing true).
  3. Take the broken printTag function from Mistake 2 (defaulted parameter before a required one) and rewrite it so it compiles, then add a third parameter int quantity = 1 that prints alongside the price and unit.

Summary

  • A default argument supplies a value for a parameter automatically when the caller omits it, chosen at compile time by the compiler based on the visible declaration.
  • Defaults are substituted at the call site — the function body has no way to know whether an argument was explicit or defaulted.
  • Once a parameter has a default, every parameter to its right must also have one; required parameters must come first.
  • A parameter’s default value may be specified only once across all declarations visible in a translation unit — conventionally in the prototype, not the definition.
  • Default arguments reduce the need for near-duplicate overloads and make optional parameters easy to skip, but should be used for values that are genuinely "usually right," not to paper over unclear APIs.