C++ / FUNCTIONS
Function overloading and overload resolution
Predict which overload a C++ call resolves to by ranking each candidate's argument conversions, and fix ambiguous-call errors instead of guessing.
What you will learn
- Overload on parameter types, count, or reference/const form, never on return type
- Rank candidates: exact match beats promotion, promotion beats standard conversion
- Diagnose an ambiguous call and fix it with a cast or an extra exact-match overload
- Spot a block-scope declaration that hides the outer overload set before ranking starts
Understanding Function overloading and overload resolution
Two functions may share a name as long as the compiler can tell their calls apart from the argument types alone. That is why the parameter list is part of the signature the compiler compares and the return type is not: at a call like `x = f(1)`, the return type is a consequence of the choice, not an input to it. So `int f(int)` and `double f(int)` is a hard error, while `void f(int)` and `void f(double)` are two independent functions that become two distinct symbols in the object file.
Resolution runs in three stages, all before the program ever runs. Name lookup walks outward from the call site until it reaches a scope that declares the name, collects every declaration of it found there, and stops; outer scopes are not consulted afterwards. Candidates that cannot accept this many arguments, or that would need a conversion the language does not provide, are discarded. Each survivor is then ranked per argument: exact match (identity, lvalue-to-rvalue, reference binding, qualification adjustments like `int*` to `const int*`), then promotion (`char`/`short`/`bool` to `int`, `float` to `double`), then standard conversion (`int` to `long`, `double` to `float`, anything to `bool`), then user-defined conversion, then ellipsis.
A candidate wins only if it is no worse than every rival on every argument and strictly better on at least one; otherwise the call is ambiguous and the translation unit does not compile. The ranking is a fixed table, not a judgement about intent or precision, and that is where the surprises live: with only `f(long)` and `f(double)` visible, `f(1)` is ambiguous because both required conversions land in the same bucket, and there is no rule preferring the "closer" or "lossless" type. Everything is decided from the static types written in the source, so a `double` variable holding 2.0 still picks `f(double)`, and the finished binary holds a direct call to one fixed function.
<iostream>
<string>
void describe(int n) { std::cout << "int " << n << '\n'; }
void describe(double d) { std::cout << "double " << d << '\n'; }
void describe(char c) { std::cout << "char " << c << '\n'; }
void describe(const std::string& s) { std::cout << "string " << s << '\n'; }
int main() {
describe(42); // identity conversion -> describe(int)
describe(3.5); // identity conversion -> describe(double)
describe('x'); // identity conversion -> describe(char)
describe(3.5f); // float->double promotion beats float->int conversion
describe(true); // bool->int promotion beats bool->char and bool->double
describe("hi"); // only viable candidate: user-defined conversion to string
}The compiler builds a candidate set from a single scope and picks the one function whose argument conversions are cheapest by a fixed rank order, entirely at compile time.
Worked examples
Equal-rank conversions tie
Shows why a two-overload set can fail to resolve a plain int, and the two ways out.
<iostream>
void take(long v) { std::cout << "take(long) " << v << '\n'; }
void take(double v) { std::cout << "take(double) " << v << '\n'; }
void pick(int v) { std::cout << "pick(int) " << v << '\n'; }
void pick(long v) { std::cout << "pick(long) " << v << '\n'; }
int main() {
take(10L); // argument is already long
take(2.5); // argument is already double
// take(10); // error: ambiguous call
take(static_cast<long>(10)); // cast turns one candidate into an exact match
pick(10); // exact match wins outright
}Example explained
Line 1take(10L): the literal's type is long already, so the conversion sequence is the identity and ranks Exact Match.
Line 2take(10) is rejected: int to long is an integral conversion and int to double is a floating-integral conversion, both Conversion rank, so neither candidate is better on the only argument.
Line 3static_cast<long>(10) changes the argument's static type, so take(long) needs no conversion and take(double) becomes strictly worse.
Line 4pick(10) needs no cast because an Exact Match candidate always outranks a Conversion-rank one.
Resolving on value category and const
Overloads that differ only in reference form are selected by whether the argument is a modifiable lvalue, a const lvalue, or an rvalue.
<iostream>
<utility>
void handle(int&) { std::cout << "int&\n"; }
void handle(const int&) { std::cout << "const int&\n"; }
void handle(int&&) { std::cout << "int&&\n"; }
int main() {
int a = 1;
const int b = 2;
handle(a);
handle(b);
handle(3);
handle(std::move(a));
}Example explained
Line 1handle(a): all three bindings are Exact Match rank, but int&& cannot bind to an lvalue, and between the two lvalue references the less cv-qualified one wins.
Line 2handle(b): int& is dropped because binding a non-const reference to a const object would discard qualifiers, leaving const int& as the only lvalue candidate.
Line 3handle(3): the literal is an rvalue, and binding an rvalue reference to an rvalue is explicitly ranked better than binding const int& to it.
Line 4handle(std::move(a)): std::move only changes the expression's value category to xvalue, which is enough to move the call to the int&& overload.
A local declaration hides the whole overload set
Demonstrates that name lookup stops at the first scope containing the name, so a nearer declaration can remove better overloads from consideration.
<iostream>
void log(int n) { std::cout << "log(int) " << n << '\n'; }
void log(double d) { std::cout << "log(double) " << d << '\n'; }
void demo() {
void log(double); // redeclares the global one, and hides both here
log(7);
}
int main() {
log(7);
demo();
}Example explained
Line 1In main, both declarations are visible at namespace scope, so log(int) wins the call with an identity conversion.
Line 2The block-scope declaration in demo introduces the name log into demo's scope; lookup finds it there and never reaches the global scope.
Line 3The candidate set inside demo therefore has exactly one member, and 7 is converted to 7.0 before the call.
Line 4Default stream formatting prints the double 7.0 as 7, which is the only visible hint that a conversion happened.
Important notes
Overload resolution is compile-time name resolution and has nothing to do with virtual dispatch: the overload is fixed by the static types of the arguments even when the runtime object is a derived type.
Top-level const on a by-value parameter is ignored in the signature, so `f(int)` and `f(const int)` declare the same function and defining both is a redefinition error.
Common mistakes
Trying to overload on the return type alone, as in `int value();` and `double value();`, which is rejected outright because the return type is not part of the compared signature.
Offering only `f(long)` and `f(double)` and then calling `f(1)`: int to long and int to double are both Conversion rank, so the call is ambiguous until an `f(int)` overload exists.
Placing a better overload (or its header include) after the call site: lookup only sees declarations visible at that point, so a worse overload is silently chosen instead of the intended one.
Try it yourself
Change, predict, then run
Write print(int), print(double), and print(const char*) overloads that each print their own tag, then call print with 5, 5.0f, 'a', true, and "five" and predict every tag before running. Now delete print(int), add print(long), and work out which calls stop compiling and why.
Open the C++ workspaceCheck your understanding
Only `void g(float);` and `void g(long double);` are visible, and you call `g(2.0)`. What happens?
- g(long double) runs, because double to long double is a floating-point promotion and promotions outrank conversions.
- g(float) runs, because it is declared first and the compiler takes the first viable candidate.
- The call is ambiguous and fails to compile, because both candidates need an equal-rank floating-point conversion.
- g(long double) runs, because the compiler prefers the conversion that cannot lose precision.
Show answer
The only floating-point promotion in the language is float to double; both double to float and double to long double are ordinary floating-point conversions, so the two candidates tie at Conversion rank on the single argument and neither is better. Option 0 is tempting because promotion genuinely does outrank conversion, but widening a double does not qualify as a promotion. Declaration order never influences resolution, and there is no rule preferring a lossless conversion.