C++ • Functions and Program Structure

Default Arguments — Use default arguments (intro)

Simplify function interfaces by letting callers omit trailing arguments that have sensible preset values.

Programming braces with a command terminal and binary digits.
Inside this lesson 10 sections 5 questions 7 figures

SECTION 1

Historical Context & Motivation

Programming languages have long wrestled with the tension between expressive function signatures and the cognitive burden placed on callers. In C, every argument to a function must be supplied at every call site, even when a particular value is overwhelmingly common. Bjarne Stroustrup recognized that this rigidity forces either redundant typing or a proliferation of near-identical function overloads, and he introduced default arguments as a core feature of C++ to address the problem directly. The idea was borrowed partly from Algol-68 and Ada, which already allowed optional parameters in various forms, but Stroustrup adapted the mechanism to mesh cleanly with C++'s call-by-value semantics and its evolving type system.

1979 • C with Classes

Bjarne Stroustrup begins developing 'C with Classes' at Bell Labs. Early prototypes already experiment with default parameter values to reduce the overload explosion caused by constructors with many options.

1985 • The C++ Programming Language, 1st Edition

Default arguments are documented as a first-class language feature. The book demonstrates their use in I/O stream constructors and mathematical utility functions, establishing conventions still followed today.

1998 • ISO C++98 Standard

The first ISO standard formalizes the rules for default arguments: they must appear in trailing positions, may be specified in declarations (not just definitions), and are evaluated at the call site. These rules remain essentially unchanged through C++23.

2011–2023 • Modern C++ Refinements

While the core semantics are stable, modern standards introduce constexpr defaults, brace-enclosed initializer defaults, and interactions with concepts and template parameters, broadening the feature's applicability.

The central question default arguments answer is deceptively simple: how can we design a function with several parameters yet keep the common-case call short and readable? Without default arguments, the alternatives are writing multiple overloaded functions (which multiplies code to maintain) or requiring callers to pass placeholder values they rarely need to change. Default arguments elegantly collapse both problems into a single declaration.

SECTION 2

Core Principles & Definitions

A default argument is a value specified in a function's declaration that the compiler inserts automatically when the caller omits the corresponding argument. The feature is governed by a small set of rules that, once internalized, make the mechanism entirely predictable. Understanding these principles is essential before studying more advanced topics such as function overloading resolution or variadic templates.

1

Trailing-Parameter Rule

Default values must be assigned from the rightmost parameter leftward with no gaps. Once a parameter has a default, every parameter to its right must also have one.
2

Declaration-Site Specification

Default values are typically placed in the function declaration (header file), not the definition. If a function is declared multiple times, each successive declaration may add defaults for parameters further to the left—but no parameter may receive a default twice.
3

Call-Site Evaluation

Default argument expressions are evaluated at the call site each time the function is invoked, not once at definition time. This means side-effecting expressions will execute on every call that triggers the default.
4

Type Compatibility

The default value must be implicitly convertible to the parameter type. The compiler performs the usual implicit conversions (integral promotions, user-defined conversions, etc.) when substituting the default.
5

Scope & Visibility

Names used in default arguments are looked up at the point of the declaration and bound at definition time, following the same scoping rules as other expressions at file or namespace scope.
✦

KEY TAKEAWAY

Think of default arguments like the factory settings on an audio equalizer. Most listeners leave the bass, mid, and treble at their preset values—only audiophiles tweak them. Similarly, a well-designed function exposes sensible defaults so that the common case requires minimal effort, while power users can override any parameter when the situation demands it.

SECTION 3

Visual Explanation — How the Compiler Fills In Defaults

The diagram below illustrates what happens when a function with three parameters—two of which have default values—is called with varying numbers of arguments. The compiler matches supplied arguments left to right and fills in defaults for any trailing parameters the caller omits.

Function Declaration with Default Argumentsvoid render(int width, int height = 480, int fps = 30);Call ScenariosCall A — 3 argsrender(1920, 1080, 60);All supplied by callerCall B — 2 argsrender(1920, 1080);fps → default 30Call C — 1 argrender(1920);height → 480, fps → 30Effective Arguments Received by FunctionCall ACall BCall Cwidth192019201920height10801080480fps603030Caller-suppliedCompiler-inserted default
Call A provides all three arguments explicitly. Call B omits fps, so the compiler inserts the default value 30. Call C omits both height and fps, receiving 480 and 30 respectively. The highlighted cells mark compiler-inserted defaults.

Notice that the compiler always matches arguments strictly left to right. There is no mechanism in C++ to skip a middle parameter while supplying a later one—if you want to override fps without overriding height, you must still supply height explicitly. This left-to-right filling rule is why the trailing-parameter constraint exists: allowing gaps would create syntactic ambiguity about which parameter each argument binds to.

SECTION 4

How Default Arguments Work Under the Hood

Default arguments are entirely a compile-time mechanism. When the compiler encounters a function call that omits one or more trailing arguments, it rewrites the call internally to include the default expressions. There is no runtime dispatch, no hidden parameter table, and no performance penalty—the generated machine code is identical to a call where every argument is written out explicitly.

Syntax Pattern

DECLARATION SYNTAX

returntypefunctionname(T1p1,T2p2=val2,T3p3=val3);return_type function_name(T₁ p₁, T₂ p₂ = val₂, T₃ p₃ = val₃);

T₁, T₂, T₃ are parameter types. val₂ and val₃ are compile-time-evaluable expressions convertible to T₂ and T₃ respectively. Parameters with defaults must be contiguous from the rightmost position.

Rewriting at the Call Site

CALL-SITE TRANSFORMATION

functionname(arg1)→functionname(arg1,val2,val3)function_name(arg₁) → function_name(arg₁, val₂, val₃)

The compiler textually substitutes val₂ and val₃ at every call site where they are omitted. This means the default expressions are evaluated in the caller's context, not in the callee's.

Declaration vs. Definition Placement

A crucial subtlety is where the default is specified. In multi-file projects, the declaration in the header typically carries the default, while the definition in the .cpp file does not repeat it. Repeating a default in both the declaration and the definition is a compilation error in standard C++. However, successive re-declarations of the same function may add defaults for previously non-defaulted parameters, as long as each parameter receives its default exactly once. This technique, while legal, is uncommon and can harm readability.

⚠

Common Pitfall

If you place the default value in the function definition rather than the declaration, any translation unit that includes only the header will not see the default and will require all arguments. Always place defaults in the declaration visible to callers—typically the header file.

SECTION 5

Detailed Rules & Edge Cases

While the basic concept is straightforward, several rules govern how default arguments interact with overloading, templates, and virtual functions. The diagram below categorizes the most important constraints, and the subsequent table provides quick-reference examples of legal and illegal declarations.

Default Argument Rules — Decision FlowchartDoes parameter have a default?NoYesCaller MUST supply argumentCaller MAY omit argumentAre ALL params to the right also defaulted?NoYes❌ COMPILATION ERROR✅ VALID DECLARATIONCould it be ambiguous with an overload?YesNo❌ AMBIGUOUS CALL✅ OK
This flowchart traces the compiler's reasoning when it encounters a default argument. The trailing-parameter rule is checked first, followed by overload ambiguity analysis.
DeclarationLegal?Reason
void f(int a, int b = 5, int c = 10);✅ YesDefaults are contiguous from the right.
void f(int a = 1, int b, int c = 10);❌ NoGap: parameter b has no default but sits between defaulted parameters.
void f(int a = 1, int b = 2, int c = 3);✅ YesAll parameters defaulted—caller can call f() with no arguments.
void f(int a, int b = 5); void f(int a);⚠ RiskyBoth declarations are individually valid, but calling f(42) is ambiguous—the compiler cannot choose between them.

Common legal and illegal default-argument declarations

💡

Virtual Functions

Default arguments in virtual functions are resolved at compile time based on the static type of the pointer or reference, not the dynamic type of the object. This means a Base* pointing to a Derived object will use Base's default, even if Derived specifies a different one. Effective C++ (Item 37) recommends never redefining an inherited default argument.

SECTION 6

Worked Example — Designing a Logging Function

Consider a logging utility for a server application. Most log messages use severity level INFO and should be written to std::cout, but callers occasionally need to redirect output or escalate severity. Default arguments let us write one function that covers all cases.

Building a logMessage() Function with Defaults

  1. 1

    Step 1 — Identify the Parameters

    The function needs three pieces of information: the message text (always required), the severity level (usually INFO), and the output stream (usually std::cout). We order them from most-likely-to-vary to least-likely-to-vary, putting the required parameter first.
  2. 2

    Step 2 — Write the Declaration with Defaults

    We declare: void logMessage(const std::string& msg, int level = 0, std::ostream& out = std::cout); Here, level = 0 represents INFO and out = std::cout provides the standard output stream. Both trailing parameters have defaults.
    void logMessage(const std::string& msg, int level = 0, std::ostream& out = std::cout);
  3. 3

    Step 3 — Write the Definition (without repeating defaults)

    In the .cpp file we define: void logMessage(const std::string& msg, int level, std::ostream& out) { out << "[" << level << "] " << msg << '\n'; } Notice that the definition omits the default values. Including them here would cause a redefinition error.
  4. 4

    Step 4 — Call with Various Argument Counts

    We can now use the function in three ways: logMessage("Server started"); uses both defaults (level = 0, out = cout). logMessage("Disk full", 2); overrides level to 2 (ERROR) but still outputs to cout. logMessage("Debug trace", 0, logFile); overrides both trailing parameters.
    One function, three calling styles—no overloads needed.
  5. 5

    Step 5 — Verify Output

    Calling logMessage("Server started"); prints [0] Server started to standard output, confirming the defaults are correctly substituted by the compiler.
    [0] Server started

SECTION 7

Default Arguments vs. Function Overloading

Default arguments and function overloading can solve overlapping problems—both allow callers to invoke a function name with different argument lists. However, they differ in flexibility, maintenance cost, and suitability for different design scenarios. The table below draws a head-to-head comparison to help you choose the right tool.

CriterionDefault ArgumentsFunction Overloading
Code duplicationSingle definition—no duplication.Each overload has its own body; shared logic must be factored into a helper.
Flexibility of typesAll calls share the same parameter types.Different overloads can accept entirely different types.
Skipping middle parametersNot possible; must supply all leading arguments.Overloads can omit any combination of parameters.
Virtual functionsDangerous—default resolved by static type, not dynamic type.Safer—each overload dispatches through the vtable correctly.
ABI stabilityChanging a default can silently alter behavior without recompiling callers.Overloads are distinct symbols; changes require relinking but are explicit.

Default arguments vs. function overloading

🔑

WHEN TO CHOOSE WHICH

Use default arguments when the omitted parameters share the same types and the 'usual' value is obvious (e.g., buffer sizes, tolerance thresholds, boolean flags). Use overloading when different call patterns require genuinely different logic or different parameter types. In practice, many C++ APIs combine both: a primary overload set with default arguments within each overload.

SECTION 8

Connections to Advanced C++ Features

Default arguments are an introductory feature, but they connect directly to several advanced C++ mechanisms. Understanding these connections early prevents surprise when you encounter templates, lambdas, and the STL's liberal use of defaults.

Introductory ConceptAdvanced ExtensionKey Difference
Default argument in function declarationDefault template arguments (C++11)Template defaults apply to types and non-type parameters, evaluated at compile time during instantiation rather than at the call site.
Trailing default parametersVariadic templates & parameter packsParameter packs accept zero or more arguments of any type, offering unlimited flexibility but requiring recursive or fold-expression unpacking.
Literal default valuesconstexpr / consteval defaultsIn constexpr functions, default values can participate in compile-time evaluation, enabling zero-overhead configuration.
Default arguments in free functionsDefault capture in lambdas (C++14)Lambda default arguments (C++14 generic lambdas / C++23 explicit defaults) blur the line between closures and ordinary functions.

From introductory defaults to advanced C++ mechanisms

The STL itself is a masterclass in default-argument design. Consider std::vector<int>—the allocator template parameter defaults to std::allocator<int>, and many of vector's constructors use default arguments for initial capacity or fill values. As you move deeper into library design, you will rely on default arguments and default template parameters as complementary tools for building ergonomic, backward-compatible interfaces.

SECTION 9

Practice Problems

Question 1: CONCEPTUAL

Explain why the C++ standard requires default arguments to appear in a contiguous group starting from the rightmost parameter. What ambiguity would arise if defaults could appear on arbitrary parameters with gaps?
Show answer
Because C++ matches actual arguments to formal parameters strictly left to right, a gap would make it impossible for the compiler to determine which parameter each supplied argument corresponds to. For example, given void f(int a = 1, int b, int c = 3), a call like f(10, 20) could plausibly mean (a=10, b=20, c=3) or (a=1, b=10, c=20). The trailing-contiguous rule eliminates this ambiguity entirely: every omitted argument is unambiguously one of the trailing defaults.

Question 2: BASIC CALCULATION

Given the declaration double computeArea(double length, double width = 1.0, double height = 1.0);, determine what arguments the function body receives for each of the following calls: (a) computeArea(5.0), (b) computeArea(5.0, 3.0), (c) computeArea(5.0, 3.0, 2.0).
Show answer
(a) length = 5.0, width = 1.0 (default), height = 1.0 (default). (b) length = 5.0, width = 3.0, height = 1.0 (default). (c) length = 5.0, width = 3.0, height = 2.0 (all supplied). In each case the compiler fills in defaults from the right. The function body always sees three fully-resolved double values regardless of how many the caller provided.

Question 3: INTERMEDIATE

The following code compiles but produces surprising output. Identify the bug and explain why it occurs. struct Base { virtual void show(int n = 10) { std::cout << "Base: " << n; } }; struct Derived : Base { void show(int n = 20) override { std::cout << "Derived: " << n; } }; Base* p = new Derived; p->show();
Show answer
The output is "Derived: 10", not "Derived: 20". Virtual dispatch selects Derived::show at runtime, but the default argument is resolved at compile time based on the static type of p, which is Base*. Therefore the compiler inserts Base's default of 10. The fix is to never redefine an inherited default argument—use the same default in both classes or use the Non-Virtual Interface (NVI) idiom, where a non-virtual public function supplies the default and delegates to a private virtual.

Question 4: APPLIED

You are designing a network socket library. The function connect() needs the following parameters: host (string, required), port (int, usually 443), timeout in milliseconds (int, usually 5000), and a boolean for TLS (usually true). Write a complete function declaration using default arguments, then show the minimal call a user needs to connect to "api.example.com" over TLS on port 443 with the default timeout.
Show answer
Declaration: bool connect(const std::string& host, int port = 443, int timeoutMs = 5000, bool useTLS = true); The minimal call is connect("api.example.com"); which receives host="api.example.com", port=443, timeoutMs=5000, useTLS=true. Parameters are ordered from most-likely-to-be-overridden (host) to least-likely (useTLS). Note that if a user wants to override only useTLS without changing port or timeout, they must still supply all preceding arguments: connect("api.example.com", 443, 5000, false);.

Question 5: CRITICAL THINKING

A colleague proposes replacing all default arguments in your codebase with function overloads to improve ABI stability. Construct a balanced argument analyzing when this refactoring is justified and when it introduces more harm than benefit. Consider compilation cost, readability, and maintenance.
Show answer
The refactoring is justified for shared libraries (.so/.dll) where changing a default value in a header does not trigger recompilation of dependent binaries, silently altering behavior—overloads, as distinct mangled symbols, avoid this. However, for application-internal code, overloads multiply maintenance: a function with N defaulted parameters potentially requires 2ᴺ overloads if all combinations are needed. This increases compile time (more overload candidates), introduces opportunities for inconsistency between overload bodies, and forces readers to locate the 'canonical' implementation. A pragmatic middle ground is to use default arguments within translation units and at API boundaries that recompile together, while preferring overloads (or the NVI idiom) at stable ABI boundaries. Named parameter idioms (builder pattern) or std::optional parameters are modern alternatives that side-step both issues at the cost of slightly more ceremony.

SUMMARY

Summary

Summary

Default arguments allow a function declaration to specify preset values for trailing parameters, enabling callers to omit arguments whose values are predictable. The compiler performs a call-site substitution at compile time, so the feature incurs no runtime overhead. Defaults must form a contiguous group from the rightmost parameter leftward, and they should be placed in the function declaration (typically the header file), not the definition.

When choosing between default arguments and function overloading, prefer defaults when all call patterns share the same types and logic, and prefer overloads when different patterns need distinct types or code paths. Exercise caution with virtual functions: defaults bind to the static type, not the dynamic type, so redefining an inherited default can produce subtle bugs. Mastering default arguments prepares you for default template arguments, STL container allocators, and idiomatic C++ API design.

Varsity Tutors • C++ • Default Arguments — Use default arguments (intro)