Functions, Overloading & Default Arguments
In C, a function name is a single, unique thing. You can only ever have one area. If you need to compute
area for an int rectangle and a double rectangle, you write two functions with two names -
area_int and area_double - because the linker resolves function calls by name alone, and it will
reject two functions sharing one. C From Zero's phase on functions
covers that world: one name, one signature, forever.
C++ throws that restriction out. A function name in C++ is not required to be unique - the signature
is what has to be unique. You can write several functions called area, as long as each one takes a
different set of parameter types, and the compiler will figure out which one you meant at every call site.
This is called overloading, and it's the first genuinely new idea in this guide (phase 2 covered syntax
changes; this is a change in what the language lets you express). Understanding how the compiler
decides which overload to run - not just that it can - is the actual goal of this phase, because get it
wrong and you'll be staring at call sites wondering why the "obviously correct" function didn't run.
Overloading: same name, different job
Here's the motivating example, rewritten from the two-function C version into one overloaded C++ name:
int
double
int
Both functions are named area. The compiler looks at the types of the arguments you passed and picks
the function whose parameter types match best. area(3, 4) passes two ints, so it calls the int
overload. area(3.5, 4.0) passes two doubles, so it calls the double overload. You never wrote
area_int or area_double - one name, two jobs, and the call site reads naturally either way.
📝 Terminology. The signature of a function is its name plus its parameter types (the return type
does not count - you cannot overload two functions that differ only in return type). Overloading means:
same name, different signature. The compiler tells overloads apart internally through name mangling -
it encodes the parameter types into the symbol the linker actually sees, so area(int,int) and
area(double,double) become two distinct linker symbols even though your source code spells them the
same way. That's the mechanism C's linker lacks, and why C can't do this.
How the compiler picks: overload resolution
When you call an overloaded function, the compiler runs a process called overload resolution. For each candidate function with that name, it checks whether your arguments could work, and ranks how good the match is. Roughly, from best to worst:
- Exact match - the argument type is already exactly the parameter type (or a trivial reference/const adjustment).
- Promotion - a small, "safe" widening, like
chartointorfloattodouble. - Conversion - anything else the compiler is willing to do implicitly, like
inttodouble.
The compiler picks the candidate with the best rank. If two candidates tie for best, or none of them are viable, you get a compile error instead of a guess - C++ never silently picks a "close enough" overload when it's genuinely unsure.
void
void
int
⚠️ The ambiguous call trap. show(5.0f) above resolves cleanly because float-to-double is a
promotion, and a promotion outranks any conversion. But some calls have no single best match. Using the
same two overloads, show(5L) - passing a long - is ambiguous: long-to-int and long-to-double
are both conversions of equal rank, so neither overload is better than the other, and the compiler
refuses to guess:
error: call of overloaded 'show(long int)' is ambiguous
The fix is never to hope the compiler picks right - it's to either add the exact overload you need
(show(long)), or cast explicitly at the call site (show(static_cast<int>(x))) so there's only one
possible match. Ambiguity errors are the compiler refusing to gamble with your intent; read them as "you
need to be more specific," not as a bug in the compiler.
💡 Key point. Overload resolution looks only at parameter types, never at what the function does
or what name would read best to a human. Two overloads that do wildly different things but happen to share
a name and similar-looking parameter types is a trap you set for future readers (including future you).
Reserve overloading for functions that do conceptually the same thing on different types - like area
above, or std::max(int,int) and std::max(double,double) in the standard library - not for unrelated
operations that happen to want the same verb.
Default arguments: one function, several call shapes
C++ also lets a function supply a default value for trailing parameters, so callers can omit them:
void
int
greeting has a default, so greet("Ava") is really greet("Ava", "Hello") with the second argument
filled in for you. This is a lighter-weight alternative to overloading when the "extra" version of a
function isn't a different type of parameter, just an optional one - notice we didn't need to write a
second greet function that hard-codes "Hello".
Two rules govern default arguments, and both exist to keep a call site unambiguous:
- Defaults must be trailing. Once a parameter has a default, every parameter after it must have one
too.
void f(int a = 1, int b)does not compile - the compiler couldn't tell, inf(5), whether5was meant foraorbifawere allowed to be skipped instead. - Declare the default once. If a function has both a declaration (say, in a header) and a definition, the default argument goes in the declaration the caller actually sees - usually the header - not in both places with (potentially) different values.
// header
void ;
// source file - no default repeated here
void
⚠️ Overloading + defaults can collide. If you overload log_message(const std::string&) and give
log_message(const std::string&, int level = 1) a default, then log_message("hi") becomes ambiguous -
both candidates can satisfy that call with zero extra arguments. Pick one mechanism per situation: use
overloading when the parameter types genuinely differ, use a default argument when you just want to make
one trailing parameter optional. Mixing both for the same gap invites exactly this kind of collision.
How arguments actually get passed
One more piece belongs here before the next phase goes deep on it: by default, C++ function parameters are
passed by value, exactly like C - the function gets its own copy, and changes inside the function don't
touch the caller's variable. That's unchanged from C. What is new is that C++ also gives you references
(std::string& instead of std::string) as a cleaner alternative to C's pointer-based "pass a pointer so the
function can modify the original." You saw a reference parameter above (const std::string& text) without
it being explained - that's deliberate; Phase 5: References vs Pointers is
entirely about what that & means and why C++ programmers reach for it constantly.
Recap
- Overloading: several functions can share a name in C++ as long as their parameter types (their signature) differ - impossible in C, where the linker needs one name per function.
- Overload resolution ranks candidates by how well argument types match: exact match beats promotion beats conversion; ties or no viable match are compile errors, not guesses.
- Ambiguous calls happen when two overloads become equally good matches - fix them by adding the exact overload needed or casting explicitly at the call site, never by hoping the compiler picks right.
- Default arguments let trailing parameters be optional (
greeting = "Hello"); defaults must be trailing, and are declared once, in the declaration the caller sees. - Don't mix overloading and default arguments to cover the same gap in parameter count - it's a classic way to make a call site ambiguous.
- Parameters are still passed by value like C by default; C++'s
&reference parameters are the modern alternative to C's pointer-passing trick, covered fully next phase.
Quick check
Test yourself on the idea that makes this phase click - what actually makes two overloads distinct, and how the compiler breaks ties:
[
{
"q": "You write `int area(int w, int h)` and `double area(int w, int h)` - same parameters, different return type. What happens?",
"choices": [
"It compiles fine - the compiler picks whichever version the caller assigns the result to",
"It fails to compile - return type alone doesn't make a valid overload, only the parameter types (the signature) do",
"It compiles, but only the `int` version is ever callable"
],
"answer": 1,
"explain": "The signature is name plus parameter types; return type isn't part of it, so two functions differing only in return type are a duplicate definition, not an overload."
},
{
"q": "Given `void show(int x)` and `void show(double x)`, what does `show(5.0f)` call?",
"choices": [
"show(int) - the float gets truncated to fit",
"show(double) - float to double is a promotion, which ranks better than a conversion to int",
"Neither - a plain float argument is always a compile error"
],
"answer": 1,
"explain": "float to double is a promotion (safe widening), which beats the conversion float would need to become int, so overload resolution picks show(double)."
},
{
"q": "Which of these default-argument declarations fails to compile?",
"choices": [
"void f(int a, int b = 1)",
"void f(int a = 1, int b)",
"void f(int a = 1, int b = 2)"
],
"answer": 1,
"explain": "Defaults must be trailing - once a parameter has a default, every parameter after it needs one too, otherwise a call like f(5) couldn't tell if 5 was meant for a or b."
}
]
← Phase 3: Types, Variables & Control Flow · Phase 5: References vs Pointers →
Check your understanding 3 questions
1. You write `int area(int w, int h)` and `double area(int w, int h)` - same parameters, different return type. What happens?
2. Given `void show(int x)` and `void show(double x)`, what does `show(5.0f)` call?
3. Which of these default-argument declarations fails to compile?