Updated Jul 14, 2026

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:

#include <iostream>

int area(int width, int height) {
    return width * height;
}

double area(double width, double height) {
    return width * height;
}

int main() {
    std::cout << area(3, 4) << "\n";        // calls the int version -> 12
    std::cout << area(3.5, 4.0) << "\n";    // calls the double version -> 14
}

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:

  1. Exact match - the argument type is already exactly the parameter type (or a trivial reference/const adjustment).
  2. Promotion - a small, "safe" widening, like char to int or float to double.
  3. Conversion - anything else the compiler is willing to do implicitly, like int to double.

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 show(int x)    { std::cout << "int: " << x << "\n"; }
void show(double x) { std::cout << "double: " << x << "\n"; }

int main() {
    show(5);      // exact match on int -> "int: 5"
    show(5.0);    // exact match on double -> "double: 5"
    show(5.0f);   // float -> promotes to double (no float overload exists) -> "double: 5"
    // show('a');    // char -> promotes to int -> "int: 97" would compile
}

⚠️ 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:

#include <iostream>

void greet(std::string name, std::string greeting = "Hello") {
    std::cout << greeting << ", " << name << "!\n";
}

int main() {
    greet("Ava");                 // "Hello, Ava!"
    greet("Ava", "Good morning"); // "Good morning, Ava!"
}

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, in f(5), whether 5 was meant for a or b if a were 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 log_message(const std::string& text, int level = 1);

// source file - no default repeated here
void log_message(const std::string& text, int level) {
    std::cout << "[" << level << "] " << text << "\n";
}

⚠️ 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

  1. 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.
  2. 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.
  3. 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.
  4. Default arguments let trailing parameters be optional (greeting = "Hello"); defaults must be trailing, and are declared once, in the declaration the caller sees.
  5. 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.
  6. 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?