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.