Default Arguments
A default argument lets the caller omit a trailing parameter, with the compiler substituting a value supplied at the declaration — a lightweight alternative to writing several overloads.
Why it matters
Defaults shrink call sites and version APIs gracefully: add a trailing int flags = 0 and every existing caller still compiles. But they are a call-site mechanism, not a runtime one — the default is baked in where the function is called, which interacts badly with virtual dispatch and across header/library boundaries. Knowing this prevents a classic family of “wrong default got used” bugs.
How it works
- Defaults bind right-to-left: once a parameter has one, every parameter after it must too.
- Put the default on the declaration in the header, exactly once; do not repeat it on the definition.
- The expression is evaluated per call, at the call site, in the caller’s scope —
f()withint x = next_id()callsnext_id()each time. - A later declaration in the same scope may add defaults to trailing params, but never redefine an existing one.
void connect(std::string host, int port = 443, int timeout_ms = 5000);
connect("a.com"); // port=443, timeout=5000
connect("a.com", 8080); // timeout=5000
connect("a.com", , 100); // ERROR: cannot skip middle argumentWhen you need to vary a non-trailing argument, reach for function-overloading instead.
Example
struct Base { virtual void f(int x = 10) { std::cout << x; } };
struct Derived: Base { void f(int x = 20) override { std::cout << x; } };
Base* p = new Derived;
p->f(); // prints 10, NOT 20The default 10 is chosen statically from Base* (the static type), while the body dispatches dynamically to Derived::f — a notorious trap.
Pitfalls
- Default + virtual picks the default from the static type and the body from the dynamic type; never give virtuals defaults.
- Different defaults in different headers for the same function are an ODR violation — silently uses whichever TU’s value.
- Defaults interfere with overload resolution:
f(int=0)andf()together makef()ambiguous. - A default that allocates or calls runs on every invocation;
= std::string{}constructs a temporary each call — cheap, but not free.