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

Function Overloading in C++: Same Name, Different Parameters

Learn how C++ function overloading works, how the compiler selects which overload to call, and how to avoid the ambiguity errors and surprising conversions that trip up new programmers.

By the end of this page you will write overloaded functions with distinct parameter types, understand how the compiler picks the right overload, and recognize when ambiguity errors occur and how to fix them.

What and Why

Overloading lets you give the same name to multiple functions that differ in their parameter types or count. Instead of writing print_int, print_double, print_string, you write three functions all named print β€” the compiler picks the right one based on what you pass:

cpp
#include <iostream>
#include <string>

void print(int x)               { std::cout << "int: "    << x << '\n'; }
void print(double x)            { std::cout << "double: " << x << '\n'; }
void print(const std::string& x){ std::cout << "string: " << x << '\n'; }

int main() {
    print(42);          // int: 42
    print(3.14);        // double: 3.14
    print("hello");     // string: hello
}

The return type is not part of the overload signature β€” two functions that differ only in return type do not compile.

Step by Step

What makes an overload distinct

Two overloads must differ in at least one of:

  • Number of parameters
  • Types of parameters (after const/volatile on top-level values are stripped)
  • Whether a parameter is an lvalue or rvalue reference (C++11)
cpp
// Valid overloads
void f(int);
void f(double);
void f(int, double);
void f(int&);         // lvalue reference overload
void f(int&&);        // rvalue reference overload (C++11)

// Invalid β€” differs only in return type, does not compile
int  g(int);
void g(int);   // error: redeclaration

How the compiler picks an overload

The compiler goes through three phases in order, stopping at the first phase that gives exactly one match:

  1. Exact match β€” the argument type matches the parameter type without any conversion.
  2. Promotion β€” small types like short, float, char are promoted to int or double.
  3. Standard conversion — int→double, pointer adjustments, derived-to-base.
cpp
void f(int)    {}
void f(double) {}

f(42);      // exact match: int
f(3.14);    // exact match: double
f('A');     // promotion: char β†’ int β†’ f(int)
f(42L);     // conversion: long β†’ int or long β†’ double β€” AMBIGUOUS, compile error

When two overloads are equally good (same conversion rank), the call is ambiguous and the program does not compile.

Overloading on const and references

Overloading const references and non-const references lets you provide different behaviour for modifiable and read-only objects:

cpp
class Buffer {
    char* data_;
public:
    char& operator[](std::size_t i)       { return data_[i]; }  // writable
    char  operator[](std::size_t i) const { return data_[i]; }  // read-only
};

Buffer b;
b[0] = 'x';           // calls non-const overload

const Buffer cb = b;
char c = cb[0];       // calls const overload β€” cannot write through const

Overloading with lvalue and rvalue references (C++11)

This pattern is common for constructors and assignment operators, but also useful for regular functions when the argument may be a temporary:

cpp
#include <string>
#include <utility>
#include <iostream>

void log(const std::string& msg) {
    std::cout << "[copy] " << msg << '\n';
}

void log(std::string&& msg) {
    std::cout << "[move] " << msg << '\n';
}

std::string s = "hello";
log(s);                       // lvalue β†’ log(const string&)
log(std::move(s));            // rvalue β†’ log(string&&)
log("temporary string");      // rvalue β†’ log(string&&)

Common Patterns

Overloading for different argument counts

cpp
void draw(int x, int y);                     // draw at specific position
void draw(int x, int y, int radius);         // draw circle
void draw(int x, int y, int w, int h);       // draw rectangle

Default arguments vs. overloading

Default arguments are often simpler than overloading when the difference is just "with or without the last argument":

cpp
// Using default argument β€” simpler
void connect(const std::string& host, int port = 8080);

// Using overloads β€” more verbose, same effect
void connect(const std::string& host);
void connect(const std::string& host, int port);

Prefer default arguments when the extra parameter is optional and the function body would be identical. Use overloads when the implementations differ significantly.

Overloading in a class vs. free functions

Member function overloads are resolved the same way as free functions. The implicit this is not part of the signature:

cpp
class Formatter {
public:
    std::string format(int x);
    std::string format(double x);
    std::string format(const std::string& x);
};

What Can Go Wrong

Ambiguous calls

cpp
void f(long)  {}
void f(float) {}

f(0);     // ambiguous: 0 is int, converts equally well to long or float
f(0L);    // unambiguous: exact match for long
f(0.0f);  // unambiguous: exact match for float

The fix is to use explicit casts or provide an exact-match overload.

Overloading vs. hiding in derived classes

In a derived class, defining a function with the same name hides all overloads in the base class β€” even those with different signatures:

cpp
class Base {
public:
    void greet(const std::string& name) {}
    void greet(int times) {}
};

class Derived : public Base {
public:
    void greet(double x) {}   // HIDES both Base::greet overloads!
};

Derived d;
d.greet("hello");   // error: Base::greet(string) is hidden
d.greet(3);        // error: Base::greet(int) is hidden

Bring the base overloads into scope with a using declaration:

cpp
class Derived : public Base {
public:
    using Base::greet;         // restore all Base::greet overloads
    void greet(double x) {}   // add the new one
};

Overloading on bool and pointer types

cpp
void f(bool) {}
void f(void*) {}

f(nullptr);   // C++11: nullptr β†’ void*, calls f(void*)
f(0);         // 0 β†’ bool (0 is false) β€” may surprise you
f(true);      // exact match: bool

With nullptr (C++11), f(nullptr) no longer ambiguously resolves to f(bool). Use nullptr over 0 or NULL for pointer arguments.

Non-template function wins over a template specialization

When an ordinary function and a function template are both viable candidates with equally good conversions, the compiler prefers the non-template:

cpp
void debug(int x) {
    std::cout << "overload: " << x << '\n';
}

template<typename T>
void debug(T x) {
    std::cout << "template: " << x << '\n';
}

debug(42);    // calls overload: 42 β€” non-template wins
debug(3.14);  // calls template: 3.14 β€” only template is viable here

This rule matters when you add an explicit specialization expecting to override a particular call β€” the specialization is still a template and can be beaten by a plain overload. Prefer overloads over explicit specializations to control which version is called.

std::initializer_list overloads bind preferentially in list-initialization

When both an initializer_list overload and an ordinary overload exist, braced-init always selects the initializer_list version, even when the non-list overload would be a better type match:

cpp
class Widget {
public:
    Widget(int size, bool visible);                        // (1)
    Widget(std::initializer_list<int> values);             // (2)
};

Widget a(10, true);    // calls (1) β€” parenthesis init, no list preference
Widget b{10, true};    // calls (2) β€” braced init: true converts to int,
                       //   initializer_list wins despite lossy conversion

This is why std::vector<int> v{3, 5} gives {3, 5} (two elements) but std::vector<int> v(3, 5) gives {5, 5, 5} (three fives). If you write a class with both kinds of constructor, document which syntax selects which.

Quick Reference

SituationOutcome
Exact type matchBest β€” no conversion needed
Integral promotion (char→int)Good — second priority
Standard conversion (int→double)Acceptable — third priority
Two equally-good conversionsAmbiguous β€” compile error
Non-template vs. template, equal rankNon-template wins
Braced-init with initializer_list overloadinitializer_list always wins
Same name in derived classHides all base overloads
Fix hidden overloadsusing Base::name; in derived class
Return type difference onlyNot an overload β€” compile error

Overload Resolution Rules in Detail

The compiler follows a strict ranked lookup when deciding which overload wins. Understanding this ranking lets you predict what the compiler will call β€” and diagnose "call is ambiguous" errors before they happen. The four ranks from best to worst are: exact match, promotion, standard conversion, and ellipsis (...).

Here is the full cascade in one example:

cpp
void f(int)    {}   // (1) exact match for int
void f(double) {}   // (2) exact match for double
void f(...)    {}   // (3) ellipsis β€” catch-all, worst rank

f(42);     // exact match β†’ (1)
f(3.14);   // exact match β†’ (2)
f('A');    // promotion: char β†’ int β†’ (1)  (not (3))
f(42ULL);  // standard conversion: unsigned long long β†’ int or double
           // both equally ranked β†’ AMBIGUOUS compile error

The ellipsis overload (...) is a last resort. C functions like printf use it, but in modern C++ it is rarely written deliberately β€” its main role is to define SFINAE sinks in template metaprogramming (an advanced topic). The key rule: whenever two overloads in the same rank are both viable, the compiler refuses to choose and you get an error.

Name Mangling and Why Overloads Work

C++ encodes each function's parameter types into the symbol name the linker sees β€” this is called name mangling. A function void print(int) becomes something like _Z5printi in a GCC-compiled binary, while void print(double) becomes _Z5printd. The different mangled names allow the linker to keep both functions in the same binary without a clash.

cpp
// These three functions have three distinct mangled names at link time
void print(int x)              {}   // e.g. _Z5printi
void print(double x)           {}   // e.g. _Z5printd
void print(const std::string&) {}   // e.g. _Z5printRKNSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEE

C does not mangle names, so C functions have exactly the symbol print β€” if you link C++ code against a C library you must tell the compiler not to mangle:

cpp
extern "C" {
    void c_log(const char* msg);   // link against a C library function
}

// You cannot overload c_log β€” all extern "C" functions share one link name
// extern "C" void c_log(int x);   // error: conflicts with the declaration above

You can inspect mangled names with nm or objdump -t on the compiled object file, or use the c++filt tool to demangle them back to readable form:

cpp
$ c++filt _Z5printi
print(int)

Default Arguments vs. Overloads β€” Trade-offs

Default arguments and overloads solve similar problems but have different costs. A default argument keeps one function body and one entry in the symbol table; overloads let each variant have a completely different body and can be added independently across compilation units.

Choose default arguments when:

  • The extra parameter is optional and the body is the same either way.
  • You want to add the parameter without breaking existing callers (ABI impact is minimal β€” the default is baked in at the call site, not the definition).
cpp
// One function, one mangled name
void connect(const std::string& host, int port = 8080);

connect("db.local");       // port = 8080
connect("db.local", 5432); // port = 5432

Choose overloads when:

  • The implementations differ meaningfully for different argument types.
  • You want to prevent certain argument types from being accepted (see Deleted Overloads below).
  • The overloaded versions may live in different translation units or need to be added by third-party code.
cpp
// Two overloads with distinct bodies
void render(const Bitmap& bmp);    // rasterise a bitmap
void render(const SVGPath& path);  // rasterise a vector path

One subtle ABI difference: if you later change a default argument value, all callers must be recompiled. An overload has no such requirement β€” the default is embedded at the call site for default arguments, not inside the function.

Reading and Fixing "Call Is Ambiguous" Errors

An ambiguous call error means the compiler found two or more overloads at the same conversion rank with no tiebreaker. The error message lists every viable candidate β€” reading it tells you exactly which overloads are competing.

cpp
error: call of overloaded 'f(int)' is ambiguous
note: candidate: void f(long int)
note: candidate: void f(float)

The three ways to fix an ambiguous call are:

Option 1 β€” Cast at the call site to an exact match:

cpp
void f(long)  {}
void f(float) {}

f(0);         // ambiguous
f(0L);        // unambiguous: exact match for long
f(0.0f);      // unambiguous: exact match for float

Option 2 β€” Add an exact-match overload so the compiler has a clear best:

cpp
void f(int) { f(static_cast<long>(0)); }  // delegate to long version

Option 3 β€” Remove one of the competing overloads if it was added by mistake. This is the cleanest fix when one overload is a redundant conversion sink.

When the error appears inside a template, the candidate list grows because the template may be instantiated with many types. In that case, constrain the template with requires (C++20) or std::enable_if (C++11/14) to narrow which types are accepted.

Overloading with Templates

A plain (non-template) overload always beats an equally-ranked template instantiation. The compiler first tries to find the best plain overload; if it finds one at the same rank as the best template instantiation, the plain overload wins without any tiebreaker needed.

cpp
#include <iostream>

template<typename T>
void describe(T x) {
    std::cout << "template: " << x << '\n';
}

void describe(int x) {
    std::cout << "plain overload: " << x << '\n';
}

describe(42);      // plain overload: 42   (non-template wins)
describe(42.0);    // template: 42         (only template matches double)
describe("hi");    // template: hi         (only template matches const char*)

Explicit function template specializations do not participate in overload resolution the same way β€” they are just implementations of a specific template instantiation. A specialization cannot beat a plain overload:

cpp
template<>
void describe<int>(int x) {   // specialization of the template for int
    std::cout << "specialization: " << x << '\n';
}

describe(42);   // still calls plain overload, NOT the specialization

This often surprises programmers who expect the specialization to be "more specific." The rule of thumb: prefer plain overloads over explicit specializations when you want to customise behaviour for a particular type. Use specializations only when working with traits or policy classes where overloads are not available.

Deleted Overloads

Marking an overload = delete tells the compiler to reject that specific call with a clear error instead of silently converting to another overload. This is the preferred way to prevent implicit conversions that would otherwise be legal.

cpp
void process(int x)    {}
void process(double x) = delete;   // reject floating-point arguments

process(42);     // OK
process(3.14);   // error: use of deleted function 'void process(double)'
process(3.14f);  // error: promotion float→double still hits the deleted overload

Without the deleted overload, process(3.14) would silently convert double to int and call the integer version β€” a likely bug. The deleted overload makes the programmer's intent explicit in the API.

Deleted overloads are also used to disable copy or move operations in classes:

cpp
class UniqueHandle {
public:
    UniqueHandle(const UniqueHandle&) = delete;             // no copy
    UniqueHandle& operator=(const UniqueHandle&) = delete;  // no copy-assign
    UniqueHandle(UniqueHandle&&) noexcept;                  // move is fine
};

Any type can be deleted, not just class members β€” you can delete free functions, constructor overloads, and conversion operators:

cpp
// Prevent the function from being called with a pointer accidentally
void log_value(long long x) {}
void log_value(void*)        = delete;   // catches log_value(ptr) at compile time

Operator Overloading Preview

Operators in C++ are just functions with special names β€” they follow exactly the same overload resolution rules as ordinary functions. When you write a + b, the compiler looks for operator+(a, b) as a free function or a.operator+(b) as a member function, ranks the candidates the same way, and picks the best match.

cpp
#include <string>
#include <iostream>

struct Vec2 {
    double x, y;
};

// Free function overload for operator+
Vec2 operator+(const Vec2& a, const Vec2& b) {
    return {a.x + b.x, a.y + b.y};
}

// Overload for printing
std::ostream& operator<<(std::ostream& os, const Vec2& v) {
    return os << '(' << v.x << ", " << v.y << ')';
}

int main() {
    Vec2 a{1.0, 2.0};
    Vec2 b{3.0, 4.0};
    std::cout << a + b << '\n';   // (4, 6)
}

The same ambiguity and conversion rules apply. If you define operator+(Vec2, int) and operator+(Vec2, double) and then write v + 42L, the compiler will apply the same promotion/conversion ranking. This means all the overload resolution knowledge you have learned here applies directly when you write operator overloads for your own types. A dedicated page covers operator overloading in depth β€” see the What's Next links below.

What's Next

  • References β€” how lvalue and rvalue references interact with overload resolution.
  • Classes Basics β€” operator overloading and member function overloads.
  • Overload Resolution β€” the complete compiler algorithm for picking among overloads.