Default Arguments
Specify fallback values for trailing function parameters, letting callers omit those arguments when the defaults are appropriate.
Default Argumentssince C++98A default argument is a value specified in a function declaration that the compiler substitutes for a missing trailing argument at the call site.
Overview
Default arguments let a single function signature serve multiple call patterns without requiring separate overloads. Resolution is entirely static: the compiler inserts the default expression into the call as if the caller had written it explicitly. This substitution happens in the translation unit that contains the call, using whichever declaration is visible there β which has significant implications for virtual functions and for where you place your defaults.
The governing constraint is that only trailing parameters may carry defaults. Once a parameter has a default, every parameter to its right must also carry one, supplied either in the same declaration or in a prior declaration in the same scope.
Syntax
// Declaration β defaults live here, typically in a header
void log(const std::string& msg,
int level = 0,
bool flush = false);
// All three call forms are valid
log("startup"); // level=0, flush=false
log("io error", 2); // flush=false
log("fatal", 3, true);Incremental Declaration
A later declaration in the same scope may supply defaults for parameters that still lack them, but it cannot repeat or alter an existing default:
void f(int x, int y = 10);
void f(int x = 5, int y); // OK: adds default for x; y already has one
// void f(int x = 5, int y = 99); // Error: cannot change y's default
// void f(int x = 6, int y); // Error: conflicts with x's defaultA declaration in an inner scope shadows the outer one with its own defaults for that scope's duration:
void g(int x = 1);
void example() {
void g(int x = 42); // local shadow
g(); // calls g(42)
}
// g() here resolves to g(1) againThe inner declaration must still be in the same scope as each other β you cannot reach into an enclosing scope and add a default there.
Expression Defaults
Default arguments are not restricted to literals. Any expression valid at the point of declaration is permitted, including function calls, namespace-scope variables, and sizeof expressions. Crucially, the expression is evaluated each time the default is used, not when the declaration is parsed:
int current_log_level = 0;
void trace(const char* tag, int level = current_log_level);
void demo() {
current_log_level = 3;
trace("init"); // level == 3, not 0 β evaluated at the call site
}This dynamic evaluation is powerful but can be a source of subtle bugs when the referenced state changes unpredictably.
Examples
Reducing Constructor Proliferation
Default arguments are particularly effective on constructors, where providing sensible defaults dramatically narrows the set of constructors needed:
class HttpRequest {
public:
explicit HttpRequest(std::string url,
std::string method = "GET",
int timeout = 30,
bool tls = true);
};
HttpRequest ping("https://example.com/health");
HttpRequest post("https://api.example.com/data", "POST");
HttpRequest slow("https://slow.example.com", "GET", 120);Extending APIs Without Breaking Call Sites
Adding a new trailing parameter with a default is binary-compatible from a source perspective: every existing call site continues to compile unchanged:
// Original
void render(const Scene& s, int width = 1920, int height = 1080);
// Extended β existing calls still compile without modification
void render(const Scene& s,
int width = 1920,
int height = 1080,
int msaa = 4);Preferring Defaults Over Overloads
When two overloads are semantically identical and differ only in whether a trailing argument is present, a default argument is the better tool β it eliminates duplicated intent and makes it explicit that the forms are equivalent (C++ Core Guidelines F.51):
// Avoid: two overloads that obscure their relationship
std::string join(const std::vector<std::string>& parts);
std::string join(const std::vector<std::string>& parts, char sep);
// Prefer: one function, one place to maintain
std::string join(const std::vector<std::string>& parts, char sep = ',');Reserve overloading for cases where the implementations genuinely differ based on argument type or count, not merely on presence.
Best Practices
Declare defaults in the header, not the definition. The compiler substitutes defaults based on the declaration visible at the call site. A default that appears only in the .cpp is invisible to every other translation unit β callers will see a hard error for the omitted argument.
Prefer constant expressions as defaults. A default tied to a mutable global will silently change behaviour at every dependent call site when that global changes. Use constexpr constants, literal values, or deterministic computed expressions.
Avoid defaults on virtual functions. See the pitfall below; the interaction with dynamic dispatch is surprising enough that the safest rule is to never use them on virtual member functions.
Do not use expensive or side-effectful expressions as defaults in hot paths. Because evaluation occurs at each call, a default like getCurrentTimestamp() will show up as call overhead in profiling and may have unintended side effects.
Common Pitfalls
Static Resolution Through Virtual Dispatch
Default arguments are resolved using the static type at the call site, not the runtime type. An overriding function that changes a default introduces a split: callers through a base reference see the base default while the derived implementation runs. The result is a function receiving a value neither party intended:
struct Renderer {
virtual void clear(uint32_t colour = 0xFF0000FF) const; // opaque red
};
struct GlRenderer : Renderer {
void clear(uint32_t colour = 0x000000FF) const override; // opaque black
};
void paint(const Renderer& r) {
r.clear(); // colour == 0xFF0000FF (Renderer's default)
// but GlRenderer::clear executes β receives red, not black
}The standard fix is a non-virtual wrapper that owns the default:
struct Renderer {
void clear() const { clear_impl(0xFF0000FF); }
void clear(uint32_t colour) const { clear_impl(colour); }
protected:
virtual void clear_impl(uint32_t colour) const;
};Defaults Are Prohibited on Most Overloaded Operators
With the sole exception of operator(), overloaded operators may not carry default arguments. Built-in operators have no concept of default operands, and the language preserves that consistency:
struct Vec2 {
Vec2 operator+(const Vec2& rhs) const;
// Vec2 operator+(const Vec2& rhs = Vec2{}) const; // Error
// operator() is exempt
double operator()(double t, double scale = 1.0) const; // OK
};Explicit Template Specializations Cannot Introduce Defaults
Explicit specializations of function templates may not add or alter default arguments. Defaults belong on the primary template:
template <typename T>
void process(T val, int flags = 0); // default here
template <>
void process(int val, int flags); // OK β omit the default
// void process(int val, int flags = 0); // Error: not allowed on specializationInconsistent Defaults Across Translation Units
Providing a default on the declaration in one header and a different default (or none) on the declaration in another header for the same function creates ODR-adjacent chaos: callers in different TUs silently receive different values. A single canonical declaration with all defaults is the only safe pattern.
See Also
reference/language/abstract-classesβ virtual dispatch and why defaults interact dangerously with overridingreference/language/class-templateβ default type arguments for template parameters follow analogous but distinct rulesreference/language/converting-constructorβ constructors with all-defaulted parameters and the implicit conversions they enable