Skip to content
C++
Language
Advanced

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++98

The 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:

  1. 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.

  2. 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.

  3. Rank to find the best viable function. Viable functions are compared argument-by-argument. A function F1 is better than F2 if, for every argument, F1's conversion is no worse than F2'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:

RankCategoryExamples
1 (best)Exact matchidentity, lvalue-to-rvalue, array-to-pointer, qualification adjustment (const-adding)
2Promotionchar→int, short→int, float→double
3Standard conversionint→double, derived-to-base pointer, T*→bool, numeric conversions
4User-defined conversionconversion operator or converting constructor, followed by a standard conversion
5 (worst)Ellipsismatch 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

cpp
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 double

Ambiguity

cpp
void g(long);
void g(unsigned int);

g(0);   // error: ambiguous — both require a standard conversion from int

Neither conversion is better than the other; the call is rejected.

User-defined conversions and constructor resolution

cpp
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 explicit

Making 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

cpp
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 conversion

C++20 concepts as tiebreakers

cpp
// 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 satisfied

Braced initializers and std::initializer_list priority (C++11)

cpp
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 match

This 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

cpp
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 types

Always prefer nullptr (C++11) over 0 or NULL for null pointer arguments.

Deleted functions still participate (C++11)

cpp
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 explicit on single-argument constructors and conversion operators to prevent unintended participation in implicit conversion sequences (C++11 extended explicit to conversion operators).
  • Use nullptr, not 0 or NULL, 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_list constructors to existing classes unless you fully audit all callsites for behaviour changes under braced initialisation.

Common Pitfalls

Name hiding in derived classes

cpp
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

cpp
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 overload

The 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

cpp
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

RankICS CategoryWhat it covers
1 — bestExact matchIdentity; lvalue-to-rvalue; array-to-pointer; function-to-pointer; qualification adjustments (T* to const T*, adding volatile)
2PromotionIntegral promotions (bool/char/short/wchar_t/unscoped enum → int/unsigned); float → double
3Standard conversionNumeric conversions (e.g. int → double, double → int); derived-to-base pointer; any pointer or pointer-to-member → bool; any pointer → void*
4User-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 — worstEllipsisArgument 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 A to B via operator B(), then B to C via C(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 const or volatile is considered better than one that does.

Per-Category Examples

cpp
// --- 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.

cpp
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.

cpp
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 const

Tiebreaker 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.

cpp
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 matches

Tiebreaker 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.

cpp
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 #2

Tiebreaker 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.

cpp
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 candidate

C++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

cpp
// 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_integral

What 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.

cpp
// 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 viable

Ranked Overloads via Constraint Layers (Metaprogramming Pattern)

A common C++20 pattern uses deliberately layered constraints to establish a preference ordering without ambiguity:

cpp
// 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 wins

Overload 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.

cpp
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");       // OK

explicit 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):

cpp
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 excluded

explicit(bool) — Conditional Explicitness (C++20)

cpp
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 explicit

Address-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.

cpp
#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

cpp
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 overload

The 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.

cpp
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 type

NULL 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

cpp
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 applies

The 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:

cpp
void log(std::string msg);

log("hello");     // OK: const char* → std::string is one UDC (string's constructor)

But:

cpp
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 UDC

Ref-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:

cpp
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 rvalue

An 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