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/shortint; floatdouble
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 doubleint and doublefloat 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.


See Also