Skip to content
C++
Language
since C++98
Intermediate

Function Overloading and Overload Resolution

"C++ overload resolution: candidate sets, conversion ranking, ADL, ref-qualifiers, deleted overloads, and concept constraints."

Function Overloadingsince C++98

Multiple functions may share the same name within the same scope provided their parameter lists differ in type or arity; the compiler selects the best match at each call site through overload resolution.

Overview

Overload resolution runs in three phases every time a call is compiled:

  1. Name lookup — builds the initial candidate set from all in-scope declarations with that name, plus any functions surfaced by Argument-Dependent Lookup (ADL) from the namespaces of the argument types.
  2. Viable candidate filtering — retains only candidates where every argument can be implicitly converted to the corresponding parameter type, and all required parameters are covered.
  3. Best-viable selection — ranks retained candidates by conversion quality; the uniquely best one wins. A tie is a hard compile error.

Functions cannot be overloaded on return type alone, nor on parameter names. Top-level const/volatile on value parameters is also invisible to overloading — void f(int) and void f(const int) are the same declaration.

Overloading is not just a naming convenience. It is a primary extension mechanism: the std::swap two-step (using std::swap; swap(a, b);) deliberately keeps the candidate set open so user-defined types can provide a more efficient version via ADL without modifying std::. The same pattern drives begin/end, hash, and most of the ranges machinery.


Conversion Ranking

Each argument→parameter match falls into one of five ranked tiers:

RankCategoryTypical examples
1Exact matchidentity; lvalue→rvalue; array→pointer; adding const qualification
2Promotionbool/char/short→int; float→double
3Standard conversionarithmetic conversions; derived*→base*; any pointer→void*
4User-defined conversionconverting constructor; operator T()
5Ellipsis... parameter

A candidate wins if it is at least as good as every other candidate on every argument, and strictly better on at least one. When two candidates are tied across all arguments, additional tie-breakers apply in order:

  • Non-template function preferred over a template instantiation.
  • More-specialised template preferred over a less-specialised one.
  • const-qualified member function preferred when the implicit object is const.
cpp
void f(int);     // rank-1 exact match for int literal
void f(long);    // rank-3 standard conversion from int
void f(double);  // rank-3 standard conversion from int

f(42);    // f(int)    — exact match
f(42L);   // f(long)   — exact match
f(42.0);  // f(double) — exact match
f(42.0f); // f(double) — float→double is rank-2 promotion

Examples

Ref-qualified member overloads (C++11)

Ref-qualifiers let member functions distinguish lvalue from rvalue objects. The canonical use is sinking data out of a temporary without a copy:

cpp
class Buffer {
public:
    // called on lvalue — must copy
    std::vector<std::byte> data() const& { return data_; }       // C++11

    // called on rvalue (expiring object) — move is safe
    std::vector<std::byte> data() &&     { return std::move(data_); } // C++11

private:
    std::vector<std::byte> data_;
};

Buffer make_buffer();

auto a = make_buffer().data();  // rvalue overload: zero-copy move
Buffer b;
auto c = b.data();              // const& overload: copy

ADL and overload sets as extension points

Free-function overloads in the same namespace as a type participate in ADL without any changes to the calling code:

cpp
template<typename Range>
void print_all(const Range& r) {
    using std::begin;  // fallback for standard containers
    using std::end;
    for (auto it = begin(r); it != end(r); ++it)
        std::println("{}", *it);
}

namespace geo {
    struct PointCloud { /* ... */ };
    // ADL injects these into the candidate set automatically
    const float* begin(const PointCloud& pc);
    const float* end(const PointCloud& pc);
}

geo::PointCloud pc{};
print_all(pc);  // ADL finds geo::begin / geo::end — no changes to print_all

Deleted overloads (C++11)

= delete places a tombstone in the overload set. The deleted candidate is found during resolution and then rejected, producing a clear diagnostic rather than a silent wrong-type conversion:

cpp
void process(int x);
void process(double x);
void process(bool)  = delete;  // C++11: block silent bool→int conversion
void process(char*) = delete;  // block string literal matching int

process(42);      // OK
process(3.14);    // OK
process(true);    // Error: call to deleted overload — intent is explicit
process("text");  // Error: call to deleted overload

Concepts constraints (C++20)

Prior to C++20, constraining an overload set required std::enable_if SFINAE that buried the intent in a type computation. Abbreviated function templates with concept constraints express the same constraint in the function signature:

cpp
// C++11–C++17: SFINAE — correct but noisy
template<typename T, std::enable_if_t<std::is_integral_v<T>, int> = 0>
void serialize(T x) { /* integer path */ }

// C++20: concept constraint — readable, same semantics
void serialize(std::integral auto x)       { /* integer path */ }      // C++20
void serialize(std::floating_point auto x) { /* floating-point path */ }// C++20
void serialize(std::string_view s)         { /* string path */ }

serialize(42);    // integral
serialize(3.14);  // floating_point
serialize("hi");  // string_view via conversion

Function templates and overloads

Non-template functions are preferred over template instantiations at the same conversion rank. Use overloads rather than explicit template specialisations — specialisations are not considered during overload resolution; they only come into play after the primary template has already won:

cpp
template<typename T>
void debug(T x) { std::println("T: {}", x); }

void debug(int x) { std::println("int: {}", x); }  // non-template preferred

template<typename T>
void debug(const std::vector<T>& v) {   // overload, not specialisation
    std::println("vector[{}]", v.size());
}

debug(42);                  // non-template preferred over instantiation
debug(3.14);                // template instantiation, no non-template match
debug(std::vector{1,2,3});  // vector overload wins as more specialised

Best Practices

  • Keep overload sets semantically coherent. Every function in the set should perform the same logical operation, differing only in the representation it accepts. Mixing unrelated behaviors under one name means adding a new overload can silently reroute existing call sites.
  • Use = delete instead of omission when an implicit conversion would compile but produce wrong behavior. The diagnostic is immediate and the intent is self-documenting.
  • Inject free-function overloads via ADL — place swap, begin/end, or custom serialisation hooks in the same namespace as your type. Generic code will find them without any coupling.
  • Prefer concepts (C++20) over SFINAE for constrained overloads. Concept constraints are part of the public interface, appear in IDE completions, and yield readable error messages.
  • Mark single-argument constructors explicit unless implicit conversion is a documented part of the type's contract. Silent user-defined conversions are rank-4 but still compile, and they create the most confusing ambiguities.

Common Pitfalls

Ambiguous arithmetic promotions

Both double→int and double→float are rank-3 standard conversions. Neither wins:

cpp
void h(int);
void h(float);

h(3.14);   // Error: ambiguous — double→int and double→float are equally ranked
h(3.14f);  // OK: exact match for float
h(3);      // OK: exact match for int

Fix: add a double overload, or cast explicitly at the call site.

NULL vs nullptr

NULL expands to 0 — an integer constant that matches int overloads before pointer overloads. Always use nullptr (C++11):

cpp
void log(int x);
void log(char* s);

log(NULL);    // calls log(int) — NULL is 0, an integer
log(nullptr); // calls log(char*) — nullptr_t converts to pointer, not int  // C++11

Hidden outer overloads

A declaration in an inner scope hides all outer-scope overloads with the same name — it does not extend the outer overload set:

cpp
void f(int);
void f(double);

void example() {
    void f(std::string_view);  // hides both f(int) and f(double)
    f(42);          // Error: only f(string_view) is visible here
    f("hello");     // OK
}

Restore the outer set with using ::f; before the local declaration.

Specialising instead of overloading

cpp
template<typename T> void process(T x);

// Wrong: explicit specialisation — ignored during overload resolution
template<> void process<int>(int x);

// Right: overload — participates in overload resolution normally
template<typename T> void process(std::vector<T> const& v);
void process(int x);

Explicit function template specialisations are selected only after the primary template wins; they do not compete with other overloads or templates.


Overload Resolution Step-by-Step

Overload resolution is a three-phase elimination tournament, not a simple search. Understanding each phase explains why the "best" match is sometimes surprising, and why ambiguity errors arise even when only one overload looks correct.

Phase 1 — Candidate set collection. The compiler gathers every function visible with the called name: in-scope declarations, functions made visible by using directives, and any functions surfaced by ADL from the namespaces of argument types. Template instantiations are generated on the fly and added to the set.

Phase 2 — Viable candidates. A candidate is retained only if it accepts the right number of arguments (accounting for defaults and ...) and every argument has at least a valid implicit conversion sequence to the corresponding parameter type.

Phase 3 — Best viable function. Each argument→parameter pair is assigned a conversion rank (see the table above). A candidate wins only if it is no worse than every other candidate on every argument, and strictly better on at least one. When two candidates tie across all arguments, the tie-breakers (non-template over template, more-specialised template, const qualification) are applied in order. A remaining tie is a hard compile error.

cpp
void calc(int,  int);    // (A)
void calc(long, long);   // (B)
void calc(int,  double); // (C)

calc(1, 2);      // (A): both args are exact match for int
calc(1L, 2L);    // (B): both args are exact match for long
calc(1,  2.0);   // (C): arg1 exact int, arg2 exact double — (C) wins
calc(1L, 2);     // ambiguous: (A) is better on arg2, (B) is better on arg1 — tie

Ambiguous overload errors occur whenever no single candidate is at least as good as all others on every argument. The fix is to add an overload that is an exact match for the call, or to cast at the call site to resolve the tie explicitly.


Overloading on Value Category (lvalue/rvalue ref qualifiers)

Ref qualifiers let a function distinguish whether the implicit object (or an argument) is an lvalue or an expiring temporary. This is the mechanism behind sink-and-move idioms and is distinct from const overloading. // C++11

A const T& parameter binds to both lvalues and rvalues, making it the "catch-all" overload. Adding a T&& overload creates a more-specialised candidate that wins for rvalue arguments, enabling a zero-copy path.

cpp
// Free-function overload pair                              // C++11
void process(const std::string& s) {                       // C++11
    std::println("lvalue: copying {}", s);
}
void process(std::string&& s) {                            // C++11
    std::println("rvalue: moving {}", s);
    use(std::move(s));
}

std::string name = "Alice";
process(name);             // lvalue overload: copies
process("Bob");            // rvalue overload: moves temporary
process(std::move(name));  // rvalue overload: explicit move

Ref-qualified member functions apply the same idea to the implicit object parameter. The common use is subscript operators that allow the caller to pillage a temporary container:

cpp
class Matrix {
public:
    double& operator[](size_t i) &  { return data_[i]; }       // C++11
    double  operator[](size_t i) && { return std::move(data_[i]); } // C++11
private:
    std::vector<double> data_;
};

Matrix make_matrix();
double x = make_matrix()[0];  // rvalue overload: moves element out

Prefer the const& / && pair over a single forwarding-reference template when the two code paths genuinely differ (copy vs. move). A forwarding template deduces to the wrong type more often than expected and cannot be const-overloaded cleanly.


Hidden Overloads and Name Hiding

A derived class that declares a function with the same name as a base class function hides every base class overload of that name — not just the one with a matching signature. This is name hiding, and it is independent of virtual dispatch.

cpp
struct Base {
    void foo(int x)  { std::println("Base::foo(int)"); }
    void foo(double) { std::println("Base::foo(double)"); }
};

struct Derived : Base {
    // Adding this single overload hides BOTH Base::foo overloads
    void foo(std::string_view s) { std::println("Derived::foo(sv)"); }
};

Derived d;
d.foo("hello");  // OK:  Derived::foo(string_view)
d.foo(42);       // Error: no matching function — Base::foo(int) is hidden
d.foo(3.14);     // Error: no matching function — Base::foo(double) is hidden

The fix is a using declaration that pulls the base overloads back into scope:

cpp
struct Derived : Base {
    using Base::foo;                 // restores both Base overloads
    void foo(std::string_view s) { std::println("Derived::foo(sv)"); }
};

Derived d;
d.foo(42);       // OK: Base::foo(int) is visible again
d.foo("hello");  // OK: Derived::foo(string_view)

Name hiding is not overriding. Overriding (virtual dispatch) requires the same signature; name hiding merely makes the base declarations unreachable from the derived scope without a using declaration. Confusing the two is a frequent source of bugs when extending a class hierarchy.


Overloading with Concepts (C++20)

Concepts replace enable_if SFINAE for constrained overload sets: the resolution rules are identical, but the code reads like a function signature. // C++20

With C++17 SFINAE the constraint is buried in a non-type template parameter; with C++20 abbreviated function templates it appears in the parameter type itself:

cpp
// C++17 — constraint hidden in template parameter list
template<typename T, std::enable_if_t<std::is_integral_v<T>, int> = 0>
void encode(T x) { /* integer encoding */ }

template<typename T, std::enable_if_t<std::is_floating_point_v<T>, int> = 0>
void encode(T x) { /* float encoding */ }

// C++20 — constraint is part of the function signature   // C++20
void encode(std::integral auto x)       { /* integer encoding */ }    // C++20
void encode(std::floating_point auto x) { /* float encoding */ }      // C++20

Partial ordering with requires clauses allows a more-constrained overload to win over a less-constrained one, mirroring how a more-specialised template beats a less-specialised one:

cpp
// C++20
template<typename T>
void inspect(T x) requires std::equality_comparable<T>
    { std::println("comparable: {}", x == x); }                    // C++20

template<typename T>
void inspect(T x) requires std::equality_comparable<T> && std::formattable<T, char>
    { std::println("formattable & comparable: {}", x); }           // C++20

inspect(42);          // second overload wins: int satisfies both constraints
inspect(std::mutex{}); // only first overload: mutex is not formattable

Concepts-based overloading is more readable, produces far better error messages, and participates in overload resolution exactly like unconstrained overloads — the most-constrained candidate still wins.


Overloading operator() for Callable Objects

A struct with multiple operator() overloads is a stateful, type-safe overload set that can be passed as a single callable. This pattern is essential for std::visit and heterogeneous dispatch.

The Overloaded helper (standardised idiom) merges several lambdas into one callable by inheriting all of their operator() overloads:

cpp
// C++17 — aggregate deduction + pack expansion + using-declaration
template<typename... Fs>
struct Overloaded : Fs... {        // C++17
    using Fs::operator()...;       // C++17
};
// C++17 deduction guide (not needed in C++20)
template<typename... Fs> Overloaded(Fs...) -> Overloaded<Fs...>;   // C++17

std::variant<int, double, std::string> v = 3.14;

std::visit(Overloaded{                                             // C++17
    [](int x)               { std::println("int: {}", x); },
    [](double x)            { std::println("double: {}", x); },
    [](const std::string& s){ std::println("string: {}", s); }
}, v);
// prints: double: 3.14

A named callable struct with explicit overloads is preferred when the callable needs state, a meaningful name, or when the number of overloads makes a lambda nest unreadable:

cpp
struct Comparator {
    bool operator()(int    a, int    b) const { return a < b; }
    bool operator()(double a, double b) const { return a < b; }
    bool operator()(std::string_view a, std::string_view b) const {
        return a < b;
    }
};

std::vector<int> ints{3, 1, 2};
std::sort(ints.begin(), ints.end(), Comparator{});

Friend Functions and Overloading

A friend function defined inside a class body is a non-member function that participates in ADL for the enclosing class's namespace. This is the hidden friend idiom, and it is the canonical way to overload operators that must not be members.

cpp
class Vector2D {
    double x_, y_;
public:
    Vector2D(double x, double y) : x_(x), y_(y) {}

    // Hidden friend: non-member, found only via ADL
    friend std::ostream& operator<<(std::ostream& os, const Vector2D& v) {
        return os << '(' << v.x_ << ", " << v.y_ << ')';
    }

    friend Vector2D operator+(Vector2D lhs, const Vector2D& rhs) {
        return {lhs.x_ + rhs.x_, lhs.y_ + rhs.y_};
    }
};

Vector2D a{1, 2}, b{3, 4};
std::cout << a + b << '\n';  // ADL finds both operator+ and operator<<

ADL is the reason non-member operator overloads work at all. When the compiler sees a + b, it includes the namespaces of a's and b's types in the candidate set. A hidden friend is visible only through that ADL lookup — it is not reachable by qualified name from outside the class, which prevents unintended use and avoids polluting the enclosing namespace.

Contrast with a member operator: operator<< cannot be a member of Vector2D because the left-hand operand would have to be a Vector2D, not std::ostream. Operators where the left-hand type is not the class type must be non-members. For symmetric binary operators like operator+, a non-member hidden friend also enables implicit conversions on both sides, whereas a member only allows them on the right-hand side.


See Also