Overload Resolution
How the compiler selects the single best-matching function from a set of candidates for a given call, via conversion sequence ranking.
Overload Resolutionsince C++98The compiler process that, given a set of candidate functions sharing a name, selects the single best-matching function for a call by constructing and ranking implicit conversion sequences for each argument.
Overview
Overload resolution runs after name lookup and before access checking, which has surprising consequences discussed under Common Pitfalls. It proceeds in three phases for every function call:
-
Build the candidate set. For an unqualified call, name lookup finds all visible declarations with that name, including those dragged in via argument-dependent lookup (ADL). For a member-function call, the candidates are the accessible members of the static type of the object. For an operator expression, both member and non-member candidates are gathered simultaneously.
-
Filter to viable functions. A candidate is viable if (a) it can accept the number of arguments provided (accounting for default arguments and
...parameters) and (b) for each argument there exists at least one implicit conversion sequence to the corresponding parameter type. -
Rank to find the best viable function. Viable functions are compared argument-by-argument. A function
F1is better thanF2if, for every argument,F1's conversion is no worse thanF2's, and for at least one argument it is strictly better. If no single function dominates, the call is ambiguous and ill-formed.
Return types play no role in overload resolution — only parameter types matter. This keeps call expressions context-independent: sqrt(2.0) means the same thing regardless of what you assign the result to.
Conversion Sequence Ranking
Implicit conversion sequences are ranked from best to worst:
| Rank | Category | Examples |
|---|---|---|
| 1 (best) | Exact match | identity, lvalue-to-rvalue, array-to-pointer, qualification adjustment (const-adding) |
| 2 | Promotion | char→int, short→int, float→double |
| 3 | Standard conversion | int→double, derived-to-base pointer, T*→bool, numeric conversions |
| 4 | User-defined conversion | conversion operator or converting constructor, followed by a standard conversion |
| 5 (worst) | Ellipsis | match via ... |
When two conversion sequences have the same rank, additional tiebreakers apply in order: a non-template is preferred over a template specialisation; among templates, the more specialised (by partial ordering) wins; since C++20, a more-constrained function (via concepts) is preferred over a less-constrained one.
Examples
Basic ranking
void f(int); // #1
void f(double); // #2
void f(long double); // #3
f(0); // calls #1 — exact match (identity)
f(0.0f); // calls #2 — float→double is a promotion (rank 2)
f(0.0); // calls #2 — exact match for doubleAmbiguity
void g(long);
void g(unsigned int);
g(0); // error: ambiguous — both require a standard conversion from intNeither conversion is better than the other; the call is rejected.
User-defined conversions and constructor resolution
struct Metres { explicit Metres(double v) : v(v) {} double v; };
struct Seconds { explicit Seconds(double v) : v(v) {} double v; };
void measure(Metres);
void measure(Seconds);
measure(Metres{3.0}); // unambiguous: exact match
measure(3.0); // error: no viable function — both constructors are explicitMaking a constructor explicit (C++11 formalised this keyword) removes it from implicit conversion sequences, which is often exactly the design intent.
Template vs. non-template tiebreaker
template <typename T>
void process(T x) { /* generic */ } // #1 — template
void process(int x) { /* fast path */ } // #2 — non-template
process(42); // calls #2 — non-template preferred when equally ranked
process(42L); // calls #1 — only template matches long without extra conversionC++20 concepts as tiebreakers
// C++20
template <typename T>
void encode(T x) { /* fallback */ } // #1
template <std::integral T>
void encode(T x) { /* integer-optimised path */ } // #2
template <std::unsigned_integral T>
void encode(T x) { /* unsigned-specific path */ } // #3
encode(42u); // calls #3 — most constrained wins
encode(42); // calls #2 — std::integral is more constrained than unconstrained
encode(3.14); // calls #1 — neither integral constraint satisfiedBraced initializers and std::initializer_list priority (C++11)
struct Widget {
Widget(int, int); // #1
Widget(std::initializer_list<double>); // #2
};
Widget w1(1, 2); // calls #1 — parentheses, no initializer_list candidate
Widget w2{1, 2}; // calls #2 — braces strongly prefer initializer_list
// even though #1 would be an exact matchThis rule (introduced in C++11) means that adding an initializer_list constructor to an existing class can silently change the meaning of existing braced constructions — a well-known source of subtle bugs.
nullptr vs 0 and NULL
void handle(int*); // #1
void handle(long); // #2
handle(NULL); // implementation-defined: NULL may be 0 (int) or 0L (long)
// — ambiguous on many implementations
handle(nullptr); // C++11: calls #1 unambiguously — nullptr has type nullptr_t,
// which converts to any pointer type but not to integer typesAlways prefer nullptr (C++11) over 0 or NULL for null pointer arguments.
Deleted functions still participate (C++11)
void run(int);
void run(long) = delete; // C++11
run(42); // calls run(int) — exact match
run(42L); // error: best viable is the deleted overload= delete does not remove a function from the candidate set — it causes a hard error if that function is selected. This is intentional: it lets you block specific overloads rather than leaving callers to accidentally match a worse one.
Best Practices
- Keep overload sets semantically uniform. Every function in an overload set should do the same conceptual operation. Violating this is a design error, not a language issue.
- Use
expliciton single-argument constructors and conversion operators to prevent unintended participation in implicit conversion sequences (C++11 extendedexplicitto conversion operators). - Use
nullptr, not0orNULL, to avoid integer/pointer ambiguity in overload resolution. - Prefer concepts (C++20) over SFINAE to constrain template overloads. The compiler selects the most constrained match automatically, and error messages are far clearer.
- Export complete overload sets. In C++20 modules, exporting only some overloads of a name produces a partially visible overload set that gives callers incorrect resolution results.
- Avoid adding
std::initializer_listconstructors to existing classes unless you fully audit all callsites for behaviour changes under braced initialisation.
Common Pitfalls
Name hiding in derived classes
struct Base {
void f(double);
void f(int);
};
struct Derived : Base {
void f(std::string); // hides ALL Base::f overloads in Derived scope
};
Derived d;
d.f(42); // error: only Derived::f(string) is in scope; int→string fails
d.f("hi"); // OK
// Fix: bring Base overloads back into scope
struct Fixed : Base {
using Base::f; // C++98
void f(std::string);
};Name lookup stops at the first scope that contains the name; overload resolution then operates only on that scope's candidates. A single new overload in a derived class hides all inherited overloads of that name.
Overload resolution precedes access checking
class Calc {
public:
std::string twice(std::string); // public
private:
int twice(int); // private
};
Calc c;
c.twice(42); // error: twice(int) is selected by overload resolution,
// then access checking fires — not "falls through" to string overloadThe compiler picks the best viable function first, then checks whether you may call it. If the best match is inaccessible, you get an access error rather than a fallback to the next-best function.
Implicit conversions masking wrong-type bugs
void store(int64_t id, bool active);
void store(int64_t id, int flags);
store(user.id, true); // calls first overload — intended
store(user.id, 1); // calls second overload — also fine
store(user.id, ptr); // calls second overload! pointer→int is a standard conversion
// Likely a bug that compiles cleanly.Strong typedef wrappers or explicit conversion operators reduce this class of error.
Implicit Conversion Sequences in Depth
The standard defines five categories of implicit conversion sequence (ICS), ranked from best to worst. Every argument-to-parameter binding during overload resolution is classified into exactly one category; the categories impose a total order used to compare candidates.
Complete ICS Ranking Table
| Rank | ICS Category | What it covers |
|---|---|---|
| 1 — best | Exact match | Identity; lvalue-to-rvalue; array-to-pointer; function-to-pointer; qualification adjustments (T* to const T*, adding volatile) |
| 2 | Promotion | Integral promotions (bool/char/short/wchar_t/unscoped enum → int/unsigned); float → double |
| 3 | Standard conversion | Numeric conversions (e.g. int → double, double → int); derived-to-base pointer; any pointer or pointer-to-member → bool; any pointer → void* |
| 4 | User-defined conversion (UDC) | One non-explicit converting constructor or one non-explicit conversion operator, optionally preceded and/or followed by a standard or exact-match conversion |
| 5 — worst | Ellipsis | Argument matched by ...; no type information is preserved |
Key points from the standard:
- An ICS at rank N beats any ICS at rank N+1 for all N.
- Within the same rank, finer tiebreakers (see next section) apply.
- A UDC sequence may contain at most one user-defined conversion step. Chaining two UDCs (e.g. converting
AtoBviaoperator B(), thenBtoCviaC(B)) is never a viable single ICS; the compiler will not chain them. - Qualification adjustments within exact-match rank are themselves ordered: a sequence that does not add
constorvolatileis considered better than one that does.
Per-Category Examples
// --- Rank 1: Exact match ---
void f(int);
void f(const int*);
int x = 5;
const int* p = &x;
f(x); // identity — no conversion at all
f(p); // qualification adjustment: int* → const int* is still rank 1
// --- Rank 2: Promotion ---
void g(int);
void g(double);
char c = 'A';
g(c); // char → int is an integral promotion (rank 2); g(int) wins
float fl = 1.0f;
g(fl); // float → double is a floating-point promotion (rank 2); g(double) wins
// --- Rank 3: Standard conversion ---
void h(long);
void h(double);
int i = 0;
// h(i); // error: ambiguous — int→long and int→double are both rank 3
void to_bool(bool);
int* ptr = nullptr;
to_bool(ptr); // T* → bool is a standard conversion (rank 3)
// --- Rank 4: User-defined conversion ---
struct Celsius {
Celsius(double d) : val(d) {} // converting constructor
double val;
};
void show(Celsius);
show(37.0); // rank 4: double → Celsius via constructor
// --- Rank 5: Ellipsis ---
void variadic(int, ...);
void variadic(int, double); // rank 3 for second arg
variadic(1, 'x'); // calls variadic(int, double): char→double (rank 3) beats ellipsis (rank 5)The One-Step UDC Rule
A critical constraint often misunderstood: the compiler applies at most one user-defined conversion per ICS. This prevents chains like A → B → C if both steps require user-defined conversions.
struct Km {
explicit Km(double v) : val(v) {}
double val;
};
struct Miles {
Miles(double v) : val(v) {} // implicit: double → Miles
operator Km() const { return Km(val * 1.60934); } // Miles → Km
double val;
};
void drive(Km);
Miles m(5.0);
drive(m); // OK: one UDC: Miles → Km via operator Km()
drive(5.0); // error: would need double→Miles then Miles→Km — two UDC steps, not allowed
// Fix: drive(Miles(5.0)) or drive(Km(8.0))Tiebreaking Rules
When two viable functions produce ICS sequences that are equal rank for every argument, the compiler applies a sequence of tiebreakers in this exact order. The first tiebreaker that distinguishes the two functions determines the winner; the rest are not consulted.
Tiebreaker 1 — Fewer Conversions Wins
Within the same rank, a sequence that performs fewer conversions is preferred. In practice this distinguishes "identity" (zero conversions) from "qualification adjustment" (one trivial conversion) within rank 1.
void p(const int&); // requires adding const — one qualification conversion
void p(int&); // binds directly — no conversion
int n = 0;
p(n); // calls p(int&): direct bind is better than adding constTiebreaker 2 — Non-template Beats Template Specialisation
When a non-template function and a function template specialisation are equally ranked for every argument, the non-template wins. This is the rule that makes non-template fast paths transparent to callers.
template <typename T>
void log(T val) { /* generic path */ }
void log(int val) { /* integer fast path */ }
log(42); // calls log(int) — non-template wins tiebreak
log(42u); // calls log<unsigned>(42u) — only template matchesTiebreaker 3 — More Specialised Template Wins
If both candidates are function template specialisations, the one that is more specialised under partial ordering of function templates wins. Partial ordering works by substituting unique synthetic types and checking which template is more restrictive.
template <typename T>
void dump(T* p) { /* pointer overload */ } // #1
template <typename T>
void dump(const T* p) { /* const-ptr overload */ } // #2 — more specialised
int x = 0;
const int* cp = &x;
dump(&x); // #1 wins: T* is a better match for int* than const T*
dump(cp); // #2 wins: const T* matches const int* exactly; #1 would add const — tiebreak goes to #2Tiebreaker 4 — Non-const Member Function Wins for Non-const Object
For member functions, if both a const and a non-const overload are viable and equally ranked, the non-const version is preferred when the object is non-const. Symmetrically, only the const overload is viable for a const object.
struct Buffer {
char* data(); // non-const: preferred for mutable Buffer
const char* data() const; // const: called when Buffer is const
};
Buffer b;
const Buffer cb;
b.data(); // calls non-const overload
cb.data(); // calls const overload — only viable candidateC++20 Tiebreaker 5 — More-Constrained Template Wins
Since C++20, a function template with a more-constrained set of requirements beats a less-constrained one when all else is equal. Constraint ordering is defined by the subsumption relation on normalised constraint expressions.
C++20 Constraints and Subsumption
Concepts impose a partial order on function templates via subsumption. Constraint P subsumes constraint Q when, in the normalised atomic-constraint form of P, every atomic constraint of Q also appears. The more-constrained candidate wins overload resolution.
Subsumption with Conjunctions
// std::integral<T> requires: std::is_integral_v<T>
// std::signed_integral<T> requires: std::integral<T> && std::is_signed_v<T>
// — std::signed_integral subsumes std::integral because it has all of
// integral's atoms plus an extra one.
template <std::integral T>
void encode(T x) { /* handles all integers */ } // #1
template <std::signed_integral T>
void encode(T x) { /* signed-integer path */ } // #2
encode(42); // calls #2 — std::signed_integral subsumes std::integral, so #2 is more constrained
encode(42u); // calls #1 — unsigned int does not satisfy std::signed_integralWhat Subsumption Does NOT Cover
Subsumption only works through concept-level references; two syntactically equivalent requires expressions written inline do not subsume each other, because the compiler does not attempt semantic comparison of arbitrary boolean expressions.
// These do NOT subsume each other, even though they express the same thing:
template <typename T> requires (sizeof(T) >= 4)
void store(T x); // #1
template <typename T> requires (sizeof(T) >= 4 && std::is_trivial_v<T>)
void store(T x); // #2 — more restrictive, but NOT more constrained via subsumption
// #1 and #2 are ambiguous when both constraints are satisfied
// Fix: define named concepts so subsumption can track the relationship:
template <typename T>
concept at_least_4 = (sizeof(T) >= 4);
template <typename T>
concept trivial_4 = at_least_4<T> && std::is_trivial_v<T>;
template <at_least_4 T> void store(T x); // #1 — less constrained
template <trivial_4 T> void store(T x); // #2 — subsumes #1; wins when both viableRanked Overloads via Constraint Layers (Metaprogramming Pattern)
A common C++20 pattern uses deliberately layered constraints to establish a preference ordering without ambiguity:
// Priority tag trick replaced by concepts in C++20:
template <typename T>
concept has_size_method = requires(T t) { t.size(); };
template <typename T>
concept has_size_member = requires(T t) { t.n; }; // hypothetical
template <has_size_method T> // preferred: richer constraint
std::size_t count(const T& c) { return c.size(); }
template <typename T> // fallback: unconstrained
std::size_t count(const T&) { return 0; }
std::vector<int> v;
count(v); // calls the constrained overload — more constrained winsOverload Resolution with Constructors
Constructors participate in overload resolution whenever an object is initialised. The form of the initialisation determines which constructors are candidates.
Copy-Initialisation vs Direct-Initialisation
Direct-initialisation (T obj(args) or T obj{args} without =) considers all constructors, including explicit ones.
Copy-initialisation (T obj = expr or passing an argument by value) excludes explicit constructors from the candidate set entirely.
struct Token {
explicit Token(int id) : id_(id) {} // explicit: not a converting constructor
Token(const char* name) : id_(0) {} // implicit: participates in copy-init
int id_;
};
Token t1(42); // OK: direct-init — explicit constructor is a candidate
Token t2 = 42; // error: copy-init excludes explicit Token(int)
Token t3 = "main"; // OK: copy-init — Token(const char*) is non-explicit
void process(Token);
process(42); // error: copy-init context — explicit Token(int) excluded
process("main"); // OKexplicit on Multi-Parameter Constructors (C++11)
Since C++11, explicit can be placed on constructors taking more than one parameter. Such a constructor is excluded from contexts that use list-initialisation as an implicit conversion (but is still a candidate for direct list-initialisation):
struct Point {
explicit Point(int x, int y) : x_(x), y_(y) {}
int x_, y_;
};
Point p1{1, 2}; // OK: direct list-init — explicit constructor selected
Point p2 = {1, 2}; // error: copy-list-init — explicit constructor excludedexplicit(bool) — Conditional Explicitness (C++20)
template <typename T>
struct Wrapper {
// Explicit only when T is not implicitly convertible from int
explicit(!std::is_convertible_v<int, T>) Wrapper(T val) : val_(val) {}
T val_;
};
Wrapper<double> w1 = 3; // OK: int is convertible to double, so not explicit
Wrapper<std::string> w2 = 3; // error: int→string is not implicit, constructor is explicitAddress-Taken Overloads
Taking the address of an overloaded function with & requires that the resulting pointer type uniquely identifies one member of the overload set. If more than one overload matches the target type, the expression is ill-formed.
Why std::for_each(..., std::isspace) Fails
std::isspace is overloaded in <cctype> and in <locale>. Even within <cctype> alone, some implementations additionally provide a locale-taking overload. The compiler cannot determine which overload to use as a function pointer, so it rejects the expression.
#include <algorithm>
#include <cctype>
#include <string>
std::string s = "hello world";
// error: can't take address of overloaded std::isspace
// std::for_each(s.begin(), s.end(), std::isspace);
// Fix 1: lambda (recommended — avoids the overload ambiguity entirely)
std::for_each(s.begin(), s.end(), [](unsigned char c) { return std::isspace(c); });
// Fix 2: explicit cast to resolve the overload manually
using IsSpaceFn = int(*)(int);
std::for_each(s.begin(), s.end(), static_cast<IsSpaceFn>(std::isspace));
// Fix 3: assign to a typed pointer first
IsSpaceFn is_space = std::isspace; // selects the int(int) overload
std::for_each(s.begin(), s.end(), is_space);Note: when casting to int(*)(int), the argument should be unsigned char at the call site to avoid undefined behaviour with negative char values — the lambda approach in Fix 1 handles this correctly.
General Rule for Address-of and Overloaded Names
int clamp(int x, int lo, int hi);
long clamp(long x, long lo, long hi);
// auto fn = &clamp; // error: ambiguous
auto fn = static_cast<int(*)(int,int,int)>(&clamp); // OK: selects int overloadThe type must be fully spelled out; the compiler does not infer which overload you want from context alone.
Additional Common Pitfalls
0 vs nullptr with Mixed Overloads
Integer literal 0 has type int, not a pointer type. When an overload set contains both f(int) and f(T*), passing 0 binds to f(int) via exact match — which is often unintended when you mean a null pointer.
void f(int n);
void f(int* p);
f(0); // calls f(int) — exact match; f(int*) would need int→int* standard conversion
f(nullptr); // calls f(int*) — nullptr_t implicitly converts to any pointer typeNULL is worse: it is implementation-defined as 0, 0L, or ((void*)0). On platforms where NULL is 0L, f(NULL) with the overload set above selects f(int) via long→int standard conversion; on others it may be ambiguous. Always use nullptr.
f(long) vs f(unsigned int) Ambiguity with int Argument
void g(long); // standard conversion from int: rank 3
void g(unsigned int); // standard conversion from int: rank 3
g(0); // error: ambiguous — both are rank 3, no tiebreaker appliesThe compiler cannot prefer long over unsigned int or vice versa; both are numeric conversions at the same rank. Add a disambiguating overload g(int) or cast the argument explicitly: g(static_cast<long>(0)).
User-Defined Conversion Chains Are Capped at One Step
As covered above, only one user-defined conversion may appear in an ICS. A common surprise arises with std::string:
void log(std::string msg);
log("hello"); // OK: const char* → std::string is one UDC (string's constructor)But:
struct Config {
operator std::string() const { return name_; }
std::string name_;
};
struct Logger {
Logger(std::string msg) {}
};
Config cfg;
Logger l = cfg; // error: Config→string (UDC1) then string→Logger (UDC2) — two UDC steps
Logger l2(std::string(cfg)); // OK: explicit first conversion, then one UDCRef-Qualified Member Overloads and Value Category
C++11 ref qualifiers let you overload on whether *this is an lvalue or an rvalue. Overload resolution compares the object's value category against & and && qualifiers:
struct Data {
std::vector<int>& items() & { return items_; } // lvalue object
std::vector<int> items() && { return std::move(items_); } // rvalue object
std::vector<int> items_;
};
Data d;
auto& ref = d.items(); // calls & overload — d is an lvalue
auto moved = Data{}.items(); // calls && overload — temporary is an rvalueAn unqualified member function (no & or &&) matches both lvalue and rvalue objects and is considered ambiguous against a ref-qualified overload of the same signature.
See Also
- Argument-Dependent Lookup — extends the candidate set for unqualified calls
- SFINAE — substitution failures silently remove template candidates from the overload set
- Converting Constructor — how single-argument constructors participate as user-defined conversion sequences
- Dependent Names — name lookup rules that interact with overload resolution in templates