C++ Part I · Module 6 · Functions
Default Arguments
The three chapters before this one were about what a parameter is and how to declare it; this one is about letting a caller not supply it. A default argument gives a parameter a value to use when the call site omits it, so a single function can serve a short call for the common case and a longer one for the callers who need control. That is a real convenience and the mechanism is small enough to learn in a paragraph. The chapter is longer than a paragraph for two reasons. The first is a set of rules about where a default may be written, which are easy to state and produce compile errors that read as baffling if you have not met them — including one that fires when you do the apparently reasonable thing of writing the default in both the declaration and the definition. The second is a fact about when the default is evaluated. Almost everyone assumes a default is a value captured at the point you declared it. It is not; it is an expression, and it runs again on every call that leaves the argument out. That difference is invisible for a default of 8 and entirely visible the moment the default is a function call, which this chapter measures.
Foundation15 min readC++Part IFundamentalsFunctions
Part I · Module 6 · Chapter 6.6 · Default Arguments
1. A Parameter With a Value to Fall Back On
#include <iostream>
static void report(int value, int width = 8, char fill = '0')
{
std::cout << "value " << value << ", width " << width << ", fill '" << fill << "'\n";
}
int main()
{
report(5);
report(5, 4);
report(5, 4, ' ');
return 0;
}Measured output:
value 5, width 8, fill '0'
value 5, width 4, fill '0'
value 5, width 4, fill ' 'One function, three call shapes. A caller who does not care about width or fill writes report(5); one who cares about width but not fill writes report(5, 4); one who cares about both supplies both.
The parameters with defaults are still ordinary parameters. Everything in Chapters 6.2, 6.4 and 6.5 applies to them unchanged — width is copied in by value, and a defaulted const& parameter is still a const&.
2. Defaults Must Be Trailing
Arguments are matched to parameters by position, left to right. So a parameter with a default cannot be followed by one without:
static void f(int a = 1, int b);error: missing default argument on parameter 'b'The reason is immediate once you try to call it. If f(7) were legal, the 7 would have to be a — and then b has no value and no default. There is no syntax for "skip this one and supply the next", so there is nothing the compiler could do.
Once a parameter has a default, every parameter after it must have one too.
The practical consequence is that the parameter order is part of the design. Put the arguments every caller supplies first, and the ones with sensible defaults last, in roughly decreasing order of how often a caller will want to override them.
3. Declare the Default Exactly Once
A function can be declared many times and defined once — Chapter 6.1. A default argument may appear in only one of those declarations.
Writing it in both the declaration and the definition looks like keeping them consistent, and is an error:
static void f(int a, int width = 8);
static void f(int a, int width = 8) { /* ... */ }error: redefinition of default argumentThe correct form puts it in the declaration and omits it from the definition:
#include <iostream>
static void f(int a, int width = 8); // the default lives here
static void f(int a, int width) // ...and not here
{
std::cout << a << " width " << width << '\n';
}
int main() { f(1); f(1, 4); return 0; }Measured output:
1 width 8
1 width 4Which declaration should carry it? The one callers can see. Chapter 2.4 established that a header holds declarations and a source file holds definitions, and a default argument is part of the interface — a caller who cannot see it cannot omit the argument. So the default belongs in the header, with the definition in the .cpp taking the parameter plainly.
That also explains a consequence worth knowing: a default argument is not part of the function. It is part of a declaration of it. Two translation units that see different declarations can get different defaults for the same function, which is a genuinely confusing bug and a good reason to declare each function in exactly one header.
4. The Default Is an Expression, Evaluated at Every Call
Here is the chapter's surprise.
A default of 8 makes it natural to think of a default as a value, fixed when you wrote it. It is not. It is an expression, and it is evaluated each time a call omits that argument.
With a default of 8 that distinction is unobservable. Make the default a function call and it becomes obvious:
#include <iostream>
static int next_width()
{
static int w = 8; // Chapter 7.4's subject; here it just counts
std::cout << " [next_width() called, returning " << w << "]\n";
return w++;
}
static void report(int value, int width = next_width())
{
std::cout << " value " << value << ", width " << width << '\n';
}
int main()
{
std::cout << "call 1:\n"; report(1);
std::cout << "call 2:\n"; report(2);
std::cout << "call 3 (explicit):\n"; report(3, 99);
std::cout << "call 4:\n"; report(4);
return 0;
}Measured output:
call 1:
[next_width() called, returning 8]
value 1, width 8
call 2:
[next_width() called, returning 9]
value 2, width 9
call 3 (explicit):
value 3, width 99
call 4:
[next_width() called, returning 10]
value 4, width 10Three things are visible in that output and all three matter.
next_width() ran three times, on calls 1, 2 and 4. If the default were captured at declaration it would have run once, before main.
It did not run on call 3. The caller supplied 99, so the default expression was never evaluated. A default you always override costs nothing.
The value changed between calls — 8, then 9, then 10. The default is not a constant; it is whatever the expression produces at the moment of the call.
The static int w inside next_width is a variable that persists between calls, which is Chapter 7.4's subject. It appears here only as a way to make each evaluation distinguishable from the last.
5. Where a Default Genuinely Helps
The frozen purpose says "where they genuinely simplify a call site", which implies there are places they do not. The cases that qualify share a shape: one obviously right value, that almost every caller wants, for a parameter that is a detail rather than a decision.
void print_hex(std::uint32_t value, int width = 8); // 8 nibbles is a 32-bit word
void dump(const SampleBlock& block, bool verbose = false);In both, omitting the argument reads as "the usual way". A caller who writes print_hex(reg) is not making a choice they should have thought about; they are accepting the only sensible one.
Two other legitimate uses:
Extending an existing function without breaking callers. Adding a parameter with a default leaves every existing call site compiling and behaving as before. That is a real and frequently decisive advantage.
Collapsing a family of near-identical wrappers. Three functions that differ only in a constant they pass through are usually one function with a default.
6. Where a Default Hides a Decision
The failure mode is a default on a parameter whose value the caller should be thinking about.
No value is obviously right. A timeout has no natural default — the right one depends entirely on what is being waited for. Defaulting it to a number picked by the author means every caller who did not think about it is now relying on a choice made by someone with less information than they have.
The default changes the function's meaning rather than a detail. void write(const Block& b, bool flush = true) is two different operations. A caller who omits the argument has silently chosen one. Two named functions are clearer than one function with a mode switch.
Several defaults in a row. void configure(int a = 1, int b = 2, int c = 3, int d = 4); forces a caller who wants to set only d to supply all four, and leaves a reader of configure(1, 2, 9) counting commas to work out which parameter 9 is. When a function reaches this shape the answer is a type that groups the settings and names them — Chapter 9.7's structs, and Chapter 9.11's chapter on choosing a representation.
A default argument is right when it saves a caller from restating the obvious, and wrong when it saves them from making a choice.
7. Defaults or Overloads?
Two different features can serve the same call sites, and the choice is worth making deliberately. Overload resolution is Chapter 6.7's subject; this is only the comparison.
void report(int value, int width = 8); // one function, a default
void report(int value); // two functions, an overload
void report(int value, int width);| Default argument | Overload | |
|---|---|---|
| Bodies | one | one per overload |
| Can differ in behaviour | no | yes |
| Can differ in parameter type | no | yes |
| Caller sees the default value | yes, in the declaration | no |
| Adding one breaks existing callers | no | no |
Prefer a default when the short call means "use the usual value". There is one body, so there is one thing to be correct, and the usual value is written down where a caller can read it.
Prefer an overload when the short call means something genuinely different — a different algorithm, a different type, or work the long form does not do. Chapter 6.7's show(int) and show(const char*) could not be one function with a default under any arrangement.
A note for later: the two features interact, and badly, when a defaulted function and an overload can both match a call. Chapter 6.7 §4 measures the ambiguity errors that result. The safe habit is to use one mechanism or the other for a given function name, not both.
8. What to Actually Do
Put defaults on trailing parameters, ordered so the most-overridden comes first among them.
Declare each default exactly once, in the header where callers can see it.
Keep defaults to literals and simple constants, so a reader can treat them as values.
Never let a default expression have a side effect, and never let it depend on when the call happens.
If the value must be computed, use a sentinel and compute it in the body, where it is visible.
Default only what is a detail, never what is a decision — and never a parameter with no obviously right value.
Reach for an overload when the shorter call means something different rather than something usual.
Use one mechanism per function name. Defaults and overloads together produce the ambiguities Chapter 6.7 measures.
9. Mistakes Worth Making Once
A non-trailing default. error: missing default argument on parameter 'b' — there is no way to skip an argument.
Writing the default in both declaration and definition. error: redefinition of default argument.
Putting the default in the .cpp. It compiles, and callers who only see the header cannot use it.
Declaring the same function with different defaults in two headers. Different translation units then get different behaviour from the same call.
Assuming the default is captured at declaration. Measured: next_width() ran on three of four calls, returning 8, 9 and 10.
Giving a default a side effect. The work is invisible at the call site and repeats per call.
Defaulting a parameter that has no obviously right value. The caller is now relying on your guess.
A row of four defaults. The caller must supply all of them to set the last, and the reader counts commas.
10. What You Should Be Able to Say Now
- A default argument supplies a value for a parameter the caller omits; one function then serves several call shapes.
- Defaulted parameters are ordinary parameters — Chapters 6.2, 6.4 and 6.5 apply unchanged.
- Measured:
report(5),report(5, 4)andreport(5, 4, ' ')gave widths 8, 4, 4 and fills'0','0',' '. - Defaults must be trailing:
error: missing default argument on parameter 'b', because arguments match by position and there is no skip syntax. - A default may appear in only one declaration:
error: redefinition of default argument. - It belongs in the header, because a caller who cannot see it cannot omit the argument.
- A default is not part of the function — it is part of a declaration, so two translation units can see different ones.
- A default is an expression evaluated at every call that omits the argument, not a value fixed at declaration.
- Measured:
next_width()ran on calls 1, 2 and 4 returning 8, 9, 10, and did not run on the call that supplied 99. - So: keep defaults to literals and simple constants, and never give one a side effect.
- If the value must be computed per call, use a sentinel and compute it in the body, where it is visible.
- A default is right when it saves a caller from restating the obvious and wrong when it saves them from making a choice.
- Prefer an overload when the shorter call means something different; prefer a default when it means the usual.
- Use one mechanism per function name — Chapter 6.7 §4 measures what happens otherwise.
11. Exercises
Exercise 1 — which calls compile? For void f(int a, int b = 2, int c = 3);, say which are legal and what each parameter gets: (a) f(1) (b) f(1, 5) (c) f(1, 5, 6) (d) f() (e) f(1, , 6).
Exercise 2 — fix the declaration. void g(int a = 1, int b, int c = 3); does not compile. Give the error, the fix, and the one-sentence reason there is no way to make the original work.
Exercise 3 — place the default. Write the header line and the source line for a function print_hex(std::uint32_t value, int width = 8), and say what goes wrong if you write the default in both.
Exercise 4 — predict the output. For §4's program, give the output if report(3, 99) is changed to report(3), including every line next_width prints.
Exercise 5 — remove the hazard. Rewrite §4's report so the width is still computed per call but the computation is visible in the body. Say what the parameter's default becomes and what the body checks.
Exercise 6 — detail or decision? For each, say whether a default is appropriate and why: (a) int width = 8 on a hex printer (b) int timeout_ms = 500 on a bus read (c) bool verbose = false on a dump function (d) bool flush = true on a write (e) char fill = '0' on a formatter.
Exercise 7 — default or overload? Choose and justify: (a) print a value, optionally with a width (b) print either an int or a string (c) sort ascending or descending (d) open a file, optionally read-only (e) compute a mean, optionally ignoring zeros.
12. Summary
- A default argument fills in a parameter the caller omits, so one function serves several call shapes.
- Defaulted parameters are ordinary parameters; the parameter-form rules are unchanged.
- Defaults must be trailing — arguments match by position and nothing can be skipped.
- Declare a default once, in the header, because it is part of the interface.
- A default is part of a declaration, not of the function.
- A default is an expression re-evaluated at every omitting call. Measured: 8, 9, 10 across three calls, and skipped on the fourth.
- So keep them to literals, never give them side effects, and use a sentinel plus body logic when a value must be computed.
- Default a detail, never a decision, and never a parameter with no obviously right value.
- Overload when the short call means something different; default when it means the usual.
- Use one mechanism per function name.
That last line is a promise this chapter cannot keep on its own, because it depends on knowing when two functions with the same name can both match a call. The next chapter is that subject. It is the point where C++ stops matching a call to the function and starts choosing among candidates, and the choosing has rules that produce answers nobody predicts the first time: a character argument that silently becomes a number, and two perfectly reasonable overloads that make an ordinary-looking call fail to compile at all because neither of them is better than the other.
Standards & specifications
- Governing standard
- ISO/IEC 14882 (the C++ language standard)(opens ISO/IEC (JTC 1/SC 22/WG 21) in a new tab)
Defines the C++ language and its standard library — the type system and templates, the object model and object lifetime, initialisation and overload resolution, the memory model and the containers, algorithms and utilities the library provides. Like C's standard it is a FAMILY of editions (C++98/03, C++11, C++14, C++17, C++20, C++23) and the editions are NOT interchangeable: a feature this curriculum relies on may be unavailable, differently specified or merely proposed in another edition, so each lesson names the edition its claim depends on. It does not define any foreign-function interface to a hardware description language, nor what a compiler actually emits, nor platform behaviour on real silicon — those are the implementation's and the device's contracts. Working drafts published by WG21 are freely available and are what most engineers read day to day; a draft is not the published standard.
This page also covers RTL structure, verification approach and debugging technique. Those are engineering practice built on the standard, not requirements the standard itself imposes.
Where this fits
Part of the C++ Programming curriculum.
