The problem overloading solves: without it, one name can only mean one thing. In C you get abs(), fabs(), labs()… the concept is identical (“absolute value”) but you’re forced to encode the type into the name. The caller has to remember which variant matches their type, and if they pick wrong, C will silently convert and maybe lose precision.
POLYMORPHISM
(same interface, many behaviors)
|
+---------------+---------------+
| |
COMPILE TIME RUNTIME
(type known statically) (type known only at runtime)
| |
+-------+-------+ +--------+--------+
| | | | | |
function operator templates virtual variant manual fn
overloading overloading (incl. dispatch + visit pointers /
CRTP) (open set) (closed type erasure
set)
Motivation
Say we want to get the absolute value for int, long and double. We could naively do this:
int iabs(int x);
long labs(long x);
double dabs(double x);
Works, but the caller does the dispatch by hand. Change a variable from int to long and every call site needs editing, and if you call iabs with a double, silent conversion occurs, which might not be what you wanted.
The solution — function overloading:
int abs(int x);
long abs(long x);
double abs(double x);
double px = -101.25;
int qty = -3;
abs(px); // compiler picks abs(double), exact match
abs(qty); // compiler picks abs(int)
How Function Overloading Is Implemented
1. Overload resolution (compile time)
When the compiler sees abs(px), it gathers all visible functions named abs (the overload set), then ranks each candidate by how well the argument types match:
- Exact match (or trivial stuff like adding
const) - Promotion (
char→int,float→double) - Standard conversion (
int→double,derived*→base*) - User-defined conversion (your converting constructors,
operator double())
The best match wins. If two candidates tie at the same rank, compilation fails with an ambiguity error rather than guessing.
Key point: resolution uses the static types of the arguments, at the call site, at compile time. Return type is ignored entirely; you can’t overload on return type alone.
2. Name mangling (link time)
The linker has no idea about overloading, it just matches symbol names. So the compiler encodes the parameter types into the symbol:
int abs(int x); // -> _Z3absi
long abs(long x); // -> _Z3absl
double abs(double x); // -> _Z3absd
double px = -101.25;
int qty = -3;
abs(px); // compiler emits: call _Z3absd
abs(qty); // compiler emits: call _Z3absi
Each overload becomes a distinct symbol, so to the linker they’re just different functions — it never sees the shared source-level name at all.
So the mechanism is: mangling makes overloads distinct functions, resolution decides which one the call site binds to. Zero runtime machinery, which is why it lands on the static side of the tree.
Aside: extern “C” Functions Can’t Be Overloaded
extern "C" means “emit this with the plain, unmangled symbol name” — that’s the whole point, it’s what lets C code call the function. But overloading only works because mangling gives each overload a distinct symbol. Strip the mangling and there’s nothing left to tell them apart:
extern "C" int scale(int x) { return 2 * x; } // symbol: scale
extern "C" long scale(long x) { return 2 * x; } // symbol: scale <- collision, error!
error: conflicting types for 'scale'
note: previous definition is here
Note the error is “conflicting types”, not “ambiguous overload”: both functions would become the same symbol scale, so the second is just an illegal redeclaration.
Operator Overloading: The Same Thing
An overloaded operator is just a function with a funny name — the operator syntax is sugar. The one requirement: at least one of the parameters must be a class type or enum type. You can’t redefine operators for built-in types alone — 2 + 3 compiles to an add instruction, and there’s no overload that can intercept it.
struct Meters { double v; };
Meters operator+(Meters a, Meters b) { return {a.v + b.v}; }
Meters d = Meters{1.5} + Meters{2.0}; // sugar for operator+(Meters{1.5}, Meters{2.0})
When the compiler sees a + b with a class- or enum-typed operand, it rewrites it as a call to a function named operator+, builds the overload set for that name, and runs the exact same overload resolution from earlier. The winner gets mangled and called directly:
_Zpl6MetersS_ <- operator+(Meters, Meters)
Operator symbols can’t appear in a linker name, so each operator gets a code instead — pl for plus — and the parameter types follow, same as before.
Same pipeline as function overloading: rewrite to a named function call, resolve at compile time, emit a direct call to a mangled symbol. Zero runtime machinery — which is why both live on the same branch of the tree.
Wrapping Up
One name, many functions: overload resolution picks the winner from the static types at the call site, name mangling bakes that choice into a distinct symbol, and by link time the shared name is gone entirely. The dispatch costs nothing at runtime because it never exists at runtime.
That covers two of the three boxes on the compile-time branch of the tree. The third — templates and CRTP — is its own post, and virtual dispatch on the runtime branch is covered here.
Appendix: What Is Name Mangling?
Step 1: We start with two functions sharing the same name:
int twice(int x) { return 2 * x; }
double twice(double x) { return 2.0 * x; }
Step 2: The compiler mangles each definition into a unique symbol.
Running nm on the object file:
_Z5twicei <- twice(int)
_Z5twiced <- twice(double)
Reading the encoding: _Z = “mangled C++ name”, 5twice = 5-char identifier “twice”, then one letter per parameter type: i = int, d = double. Parameter types in, return type out (which is why you can’t overload on return type: it wouldn’t produce a distinct symbol… well, mainly because resolution ignores it, but the mangling reflects that).
Step 3: At each call site, resolution picks a candidate, and the compiler emits a call to that specific mangled symbol.
Look at the disassembly of main with relocations:
mov $0x3,%edi ; load int 3 into arg register
call ... ; R_X86_64_PLT32 _Z5twicei <- names twice(int)
movsd 0x0(%rip),%xmm0 ; load 3.5 into float register
call ... ; R_X86_64_PLT32 _Z5twiced <- names twice(double)
Those R_X86_64_... lines are relocation entries: the object file literally says “patch this call to point at symbol _Z5twicei”. The overload decision is fully baked in here; there is no trace of “twice” as an ambiguous name anymore.
Step 4: The linker’s view. The linker never heard of overloading. It sees two ordinary jobs: “someone needs _Z5twicei, someone provides _Z5twicei, connect them.” Exact string match on symbols, same as it would do for C’s abs and labs. This is your separate-compilation model unchanged: mangling is the trick that squeezes C++‘s rich naming through the linker’s dumb string-matching interface.