Function Overloading and Overload Resolution
"C++ overload resolution: candidate sets, conversion ranking, ADL, ref-qualifiers, deleted overloads, and concept constraints."
Function Overloadingsince C++98Multiple 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:
- 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.
- Viable candidate filtering — retains only candidates where every argument can be implicitly converted to the corresponding parameter type, and all required parameters are covered.
- 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:
| Rank | Category | Typical examples |
|---|---|---|
| 1 | Exact match | identity; lvalue→rvalue; array→pointer; adding const qualification |
| 2 | Promotion | bool/char/short→int; float→double |
| 3 | Standard conversion | arithmetic conversions; derived*→base*; any pointer→void* |
| 4 | User-defined conversion | converting constructor; operator T() |
| 5 | Ellipsis | ... 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 isconst.
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 promotionExamples
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:
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: copyADL 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:
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_allDeleted 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:
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 overloadConcepts 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:
// 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 conversionFunction 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:
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 specialisedBest 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
= deleteinstead 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
explicitunless 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:
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 intFix: 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):
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++11Hidden 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:
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
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.
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 — tieAmbiguous 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.
// 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 moveRef-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:
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 outPrefer 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.
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 hiddenThe fix is a using declaration that pulls the base overloads back into scope:
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:
// 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++20Partial 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:
// 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 formattableConcepts-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:
// 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.14A 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:
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.
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.