A default argument is a value the compiler supplies for a parameter when the caller doesn't provide one. It lets a single function serve many call sites cleanly, so simple cases stay short and full-control cases still work. This lesson covers the syntax, the right-to-left rule, where defaults belong (declaration vs definition), how call resolution picks them, the ambiguities that can show up when defaults meet overloading, and when to pick defaults over overloads.
Functions often have a "usual" value for one or more parameters. Currency is almost always USD. Discount percent for a flash sale is almost always 10. Whether an order is expedited is almost always false. Without defaults, every caller has to spell out every argument, or a pile of overloads all forward to the same underlying logic.
The problem without defaults. Consider a function that formats a price for display.
Every call repeats "USD" and 2. The call sites read worse than they should. The information that matters (the price) is buried at the start of a longer argument list. With defaults, the same function can be called three ways: with just the price, with a different currency, or with a different precision.
Same function, three different call shapes. The common case is short. The full-control case is still available. That's the reason default arguments exist.
A default argument is written with = after the parameter declaration, inside the parameter list.
The function applyDiscount takes two parameters. price has no default, so every caller must supply it. percent has a default of 10.0, so callers can omit it.
A complete example:
The first call passes only the price, so percent takes the default value 10.0. The second call passes both, so the default is ignored. The function body has no idea which case it's in; the compiler patches the missing argument at the call site, not inside the function.
The default expression can be a literal (10.0, "USD", false), a const variable visible at the call site, or a function call whose result the compiler uses as the argument. The most common case by far is a plain literal.
Once a parameter has a default value, every parameter to its right must also have a default. The rule exists because of how the compiler matches arguments to parameters: positionally, left to right. If a parameter in the middle had a default but the one after it didn't, the compiler couldn't tell which argument the caller meant to skip.
This is fine:
This is not:
g++ rejects this with:
The compiler doesn't say "the order is wrong." It says the parameter after the defaulted one is missing a default, because that's literally the rule it's checking.
A slightly subtler example. Consider a function that creates a new product entry with a name, a price, an optional category, and an optional stock count. Giving category a default but leaving stockCount without one produces an invalid declaration.
g++ reports the same error: parameter 4 is missing a default. The fix is to either give stockCount a default too, or remove the default from category and force every caller to supply both.
Every caller must supply name and price. They can stop there, or add category, or add both category and stockCount. They cannot skip category and provide stockCount, because there's no way to write that call in C++.
This is the rule that causes the most confusion. Defaults belong in the declaration (the prototype, usually in a header file), not in the definition (the body, usually in a `.cpp` file). Putting them in both is an error in most setups, and putting them only in the definition means they're invisible to callers who only see the declaration.
A clean two-file setup:
The default = 10.0 appears in the header, where every caller can see it. The .cpp file defines the function. Callers #include "discount.h" and inherit the default.
Writing the default in both places makes g++ complain:
The standard says a default argument can be specified only once per translation unit for a given parameter. If the header is #included in the .cpp file (which it usually is), the default appears twice and the compiler rejects it.
The opposite mistake: default only in the definition.
This compiles, but it doesn't do the right thing. The default is only visible to code in discount.cpp (and to any file that happens to #include the .cpp file, which no one should ever do). Every other caller sees the declaration in the header, which has no default, so they're forced to supply both arguments. The default exists but is useless.
Rule of thumb: Default arguments live in the header. The definition repeats the parameter list without the = part.
In a single-file program (everything in main.cpp), the default can go on either the forward declaration or the definition, as long as it appears in only one place.
When there's no separate forward declaration and only the definition exists, that definition is also the declaration, and the default goes on it:
That works too. The point: the default appears exactly once, in whichever declaration the caller can see.
Argument matching is strictly positional. The compiler lines up arguments from left to right against parameters. When the caller runs out of arguments before the parameter list ends, the compiler fills the rest from the defaults, again left to right.
Consider this function:
The valid call shapes are:
The compiler cannot skip a middle parameter to reach a later one:
g++ says:
The compiler matched arguments left to right: "Alice" went to name, then true was matched against email, which is a std::string. There's no way to say "skip the email and use its default." C++ has no named-argument syntax. The defaults only fill in at the end of the list.
The same idea with formatPrice:
The last line fails for the same reason. Defaults are not labeled. They just fill from the right end of the parameter list as far as needed.
The flow is one-directional. Once the compiler stops pulling arguments from the call site, every remaining parameter is filled from its default, in order.
The default expression can be any expression that's valid at the point where the default is declared and produces a value compatible with the parameter type. The most common cases are literals.
A const variable or constexpr value visible at the call site also works.
A function call also works as a default. The function is invoked once per call where the default is used, not once when the program starts.
Every call to logEvent without a timestamp invokes currentTimestamp() fresh, so each call gets the current time at the moment of the call. This is sometimes useful, but it can surprise readers who expected the default to be a static value.
This rule confuses developers coming from languages like Python. In C++, a default value cannot reference another parameter of the same function. The default is evaluated at the call site, before the function is called, when the other parameters don't have values yet.
g++ rejects this with:
The default for tax is computed at the call site, before chargeOrder is called. At that moment, price is the name of a parameter that doesn't exist yet (the function call hasn't started). The compiler enforces this with a hard rule: defaults can only see names that are visible in the surrounding scope, not the function's own parameters.
For a default that depends on another argument, do it inside the function body:
That's the pattern: use a sentinel (NaN, -1, an empty string, depending on what's available) as the default, then compute the real value inside the body after detecting the sentinel. It's clunkier than the original would have been, but it's the C++ way of expressing "default depends on another argument."
A cleaner alternative is a wrapper function that supplies the dependent default and forwards to the main function:
That's two functions sharing a name (function overloading). The wrapper computes the tax from the price and forwards. No sentinel needed, no in-body check. Most C++ codebases pick this style when the dependency is real.
Function overloading allows multiple functions with the same name but different parameter lists. Defaults and overloading can both produce the same effect (a function callable with different argument counts), and putting them together can create ambiguity the compiler can't resolve.
An ambiguous case:
g++ rejects the call:
Both functions are valid candidates. The first takes one int. The second takes one int and uses the default for the second parameter. The compiler has no rule to prefer one over the other, so the call is rejected. The fix is to pick one design, not both.
Fix 1: drop the overload, keep the default.
One function, one call site shape (placeOrder(101) and placeOrder(101, true) both work). This is usually the cleanest answer.
Fix 2: drop the default, use two overloads.
Now the compiler can pick exactly one based on the argument count, and there's no overlap.
The general guidance: don't mix overloading and defaults on the same parameter position. Doing so makes ambiguity likely. Pick one tool per slot.
Another flavor of the same problem, with different types:
Same story, same fix. Either remove the default or remove the overload.
Both tools offer multiple call shapes for the same operation. They are not interchangeable. The trade-offs matter.
| Use default arguments when... | Use overloading when... |
|---|---|
| All call shapes do the same thing, just with different values for some parameters | The behavior is different for different parameter combinations |
| The parameters have the same types in all call shapes | Different types must be accepted (e.g., int and std::string) |
| A single function in the binary is preferable | Each version should compile separately, possibly with different optimizations |
| The default value is a constant | The "default" depends on other arguments or needs setup logic |
A concrete example. Consider a printReceipt function. If all variations print the same data with different formatting options (currency symbol, decimal places, line width), defaults are a good fit. The behavior is the same; only formatting parameters change.
If instead receipts need to print from completely different inputs (one takes a vector of prices, another takes a single struct, another takes a database cursor), overloading is right. Same name, different input shapes, different bodies.
The two are mixed up most often when a developer wants "the convenience of optional arguments" and uses overloading by reflex. If the bodies are nearly identical, defaults are cleaner.
A real cost worth knowing: with defaults, the default expression is compiled into every call site that uses it, not stored once in the function. That doesn't matter for simple literals, but it can matter for function-call defaults that get inlined repeatedly.
A function-call default like int x = expensive() runs expensive() once per call that omits x. If expensive() is non-trivial and the function is called from many places, the default expression is duplicated at every call site. For pure constants this is free; for function calls, prefer an explicit overload that does the work once.
A small order-management snippet that uses defaults for a few different parameter types: bool, double, std::string, and int.
Four calls, four different argument counts, one function. Customer 101 takes everything by default. Customer 102 overrides only the currency. Customer 103 adds expedited shipping. Customer 104 also gets a triple loyalty multiplier.
Parameter order matters. currency is the third parameter, expedited is the fourth, loyaltyPointsMultiplier is the fifth. A caller who wants only expedited = true still has to supply currency, because there's no way to skip a parameter. That's why the most-commonly-changed optional parameters should appear leftmost in the optional section, and the rarely-changed ones rightmost.
A common ordering rule when designing a function:
Frequent need to skip a middle parameter is a sign the parameter order needs work, or that the function should be split into multiple overloads.
When defaults live in a header, every translation unit that includes the header sees those defaults. Changing a default value requires recompiling every file that includes the header to pick up the new default. The compiler bakes the default into the call site of every caller.
Changing the default to 3.0 means every .cpp file that includes shipping.h and calls calculateShipping(weight) (without a second argument) will, after recompilation, compute the shipping at the new rate. Files that aren't recompiled still use the old default. This is a real source of bugs in larger projects when build systems don't track header dependencies correctly.
Changing a default argument is an interface change. Every translation unit that includes the header must be rebuilt to see the new value. A precompiled library that ships to callers compiled against the old header keeps using the old default until those callers rebuild.
For library APIs, this is a reason to be conservative about defaults. A default value becomes part of the API contract, and changing it can change caller behavior without warning. If a parameter's "right" value is likely to change over time, consider making it required or providing a separate "configuration" mechanism.
For application code where the header and every caller are controlled together, defaults are fine to evolve as the code evolves.
A short tour of the mistakes that show up most often when working with defaults.
Mistake 1: Default in both declaration and definition.
g++ reports default argument given for parameter ... here with a note pointing to the previous specification. The fix is to keep the default only in the header.
Mistake 2: Skipping the rightmost parameter when an earlier one has a default.
Once a has a default, b (to its right) must also have one. Move the default to b instead, or give both parameters defaults.
Mistake 3: Trying to skip a middle argument.
There's no way to write "use the default for s but supply b." The compiler walks left to right and stops when the call's argument list ends. Overriding only b requires supplying s as well.
Mistake 4: Expecting a default to depend on another parameter.
C++ defaults can't see the function's other parameters. Use a sentinel inside the body, or define an overload that computes the dependent value and forwards.
Mistake 5: Mixing defaults and overloading on the same call shape.
Both overloads can accept f(5). Pick one strategy per call shape.
A short checklist for deciding whether a default argument is the right tool:
For internal application code, defaults are a small, low-cost convenience. For library APIs, treat them as part of the contract and choose them deliberately. Either way, when defaults and overloads start to overlap on the same call shape, the design has gone too far in one direction. Step back and pick one.
10 quizzes