Course outline · 0% complete

0/27 lessons0%

Course overview →

Overloading and Default Arguments

lesson 4-3 · ~9 min · 11/27

One name, several bodies

Overloading exists because inventing a new name for every parameter type scales badly. C, which lacks it, needs abs, labs, fabs, and fabsf for the single idea of absolute value.

C++ lets one name serve them all and picks the right body from the argument types at compile time. The standard library leans on this everywhere, and std::to_string alone has nine overloads.

Same name, different parameters

C++ lets several functions share a name as long as their parameter lists differ. This is overloading, and the compiler picks the right one from the argument types at the call site:

int    twice(int x)    { return x * 2; }
double twice(double x) { return x * 2.0; }
std::string twice(const std::string& s) { return s + s; }

twice(4);       // calls the int version, 8
twice(1.5);     // calls the double version, 3.0
twice(std::string("ab"));  // "abab"

Python cannot do this, since a second def twice simply replaces the first. Note also that the return type alone is not enough to overload, so two functions differing only in return type will not compile.

Default arguments

void repeat(std::string s, int times = 2) { ... }

repeat("hi");      // times is 2
repeat("hi", 5);   // times is 5

Defaults must be the trailing parameters, the same restriction Python has. The default belongs in the declaration that callers can see, which in a multi-file program means the header, not the definition.

Three overloads and the calls that select them

Three functions named show, distinguished only by their parameter types, called once each.

#include <iostream>
#include <string>

void show(int x)    { std::cout << "int: " << x << "\n"; }
void show(double x) { std::cout << "double: " << x << "\n"; }
void show(const std::string& s) { std::cout << "string: " << s << "\n"; }

int main() {
    show(42);
    show(3.14);
    show(std::string("hello"));
    return 0;
}

Output

int: 42
double: 3.14
string: hello

Selection happens entirely at compile time, so there is no runtime cost to having three overloads instead of three differently named functions. The generated code calls exactly one of them at each site.

The third call is written as show(std::string("hello")) rather than show("hello") for a reason worth knowing. A bare "hello" is not a std::string, it is an array of char, and reaching the string overload from it requires a conversion that the compiler considers worse than converting it to a bool for an int-like match. Wrapping it makes the intent unambiguous.

Why overloads cannot differ by return type alone

The pair int parse(std::string s) and double parse(std::string s) cannot coexist. They differ only in return type, so a call like parse("5") would be ambiguous, and the language forbids the declaration outright rather than waiting for such a call.

Overload resolution looks only at the arguments at the call site, never at what the result is used for. Given parse("5"), the compiler has nothing to distinguish the two candidates, and that stays true even in a context like double d = parse("5");, because the target of an assignment is not part of the decision.

The fix is to change the name or the parameters. Names like parseInt and parseDouble are the conventional choice, and the standard library does exactly this with std::stoi and std::stod.

Overloading on argument count

Two area functions, one for a square and one for a rectangle. Here the parameter counts differ rather than the types.

#include <iostream>

double area(double side) {
    return side * side;
}

double area(double w, double h) {
    return w * h;
}

int main() {
    std::cout << area(5.0) << "\n";
    std::cout << area(3.0, 4.0) << "\n";
    return 0;
}

Output

25
12

The compiler selects by argument count here, so one argument reaches the square version and two reach the rectangle version. Differing counts are the easiest overloads to reason about, since no conversion rules come into play at all.

The output shows 25 rather than 25.0, because std::cout drops a trailing .0 when a double happens to hold a whole number, exactly as lesson 2-1 described.

This pair could also have been written as one function with a default argument, but doing so would be a mistake. A signature like double area(double w, double h = ?) has no sensible default for the height, since the square case needs the height to equal the width rather than any fixed constant. Defaults work when the omitted value is a constant, and overloads work when the omitted value has to be derived.