Method and Constructor Overloading

Learning Objectives

Declare overloads and predict which one is selected.

Explain implicit conversions, ambiguity and default arguments.

Know that the return type alone cannot distinguish C++ overloads.

Compare overloading support across languages.

Prerequisites: Modules 2, 5.

Intuition and Definition

Overloading gives several functions the same name with different parameter lists. The compiler chooses one at compile time by overload resolution: it ranks candidates by how well each argument matches (exact match > promotion > standard conversion > user-defined conversion) and picks the unique best one; if two are equally good, the call is ambiguous.

Why It Matters

Overloads let one concept (print, add, construct) accept natural argument types. Badly designed overload sets cause surprising choices (print(0) vs print(nullptr)) or ambiguity errors.

Syntax and Language Notes

Yes, by parameter types/count/qualifiers; templates and default arguments interact

Yes, by parameter types/count; resolution with boxing and varargs phases Yes, plus optional and named parameters

No — a later def replaces the earlier one; use default arguments, *args, or functools.singledispatch

Overload *signatures* for typing, but a single implementation that checks arguments at run time

void print(int value);
void print(double value);
void print(const std::string& value);

Complete Example

An ambiguous call is a compile error — both candidates need a conversion of the same rank:

Python has no overloading; functools.singledispatch dispatches on the first argument's type at run time:

#include <iostream>
#include <string>

void print(int v)                { std::cout << "int:    " << v << "\n"; }
void print(double v)             { std::cout << "double: " << v << "\n"; }
void print(const std::string& v) { std::cout << "string: " << v << "\n"; }
void print(const char* v)        { std::cout << "c-str:  " << v << "\n"; }
void print(bool v)               { std::cout << "bool:   " << std::boolalpha << v << "\n"; }

class Duration {
    long long ms_;
public:
    Duration() : Duration(0) {}                                         // overloaded constructors
    explicit Duration(long long ms) : ms_(ms) {}
    Duration(int minutes, int seconds) : ms_((minutes * 60LL + seconds) * 1000) {}
    long long ms() const { return ms_; }
};

int main() {
    print(42);              // exact match int
    print(3.5);             // exact match double
    print(2.5f);            // float -> double is a promotion
    print('A');             // char -> int is a promotion
    print("literal");       // const char* exact match beats conversion to std::string
    print(std::string("s"));
    print(true);
    print(static_cast<short>(7));   // short -> int promotion

    std::cout << Dura
…
int:    42
double: 3.5
double: 2.5
int:    65
c-str:  literal
string: s
bool:   true
int:    7
0 1500 150000
void scale(float f) { (void)f; }
void scale(double d) { (void)d; }
int main() {
    scale(10);      // int -> float and int -> double are both standard conversions
}
m15_ambiguous.cpp:4:5: error: call to 'scale' is ambiguous
from functools import singledispatch

@singledispatch
def describe(x):
    return f"something: {x!r}"

@describe.register
def _(x: int):
    return f"int {x}"

@describe.register
def _(x: list):
    return f"list of {len(x)}"

print(describe(5), "|", describe([1, 2, 3]), "|", describe(2.5))

def area(w, h=None):          # default argument instead of two overloads
    return w * (w if h is None else h)
print(area(3), area(3, 4))
int 5 | list of 3 | something: 2.5
9 12

Step-by-Step Walkthrough

print(2.5f) — no float overload, but float→double is a *promotion*, better than float→int conversion.

print('A') — char→int is a promotion, so the int overload wins (prints 65); there is no char overload.

print("literal") — const char* is an exact match (after array-to-pointer decay); std::string would need a user-defined conversion, ranked lower.

Constructors overload the same way: Duration(2, 30) selects the two-int version.

scale(10) is ambiguous: int→float and int→double are both *standard conversions* with equal rank.

Common Mistakes

Overloading on types that convert into each other (int and bool, const char* and std::string and std::string_view) — confusing selections.

Expecting overloads to dispatch on dynamic type — resolution uses the *static* types of arguments.

Different behaviour in overloads of the same name — overloads should do "the same thing" for different inputs.

Default arguments + overloads creating ambiguous calls.

Python: defining two def f versions — only the last survives.

Trade-offs and Alternatives

Templates (template <typename T> void print(const T&)) avoid writing N overloads but accept types you didn't intend unless constrained by concepts (Module 17).

Named factory functions (Duration::fromSeconds) beat ambiguous constructor overloads with the same parameter types.

Real-World Use

std::to_string, std::abs, stream operator<<, logging APIs (log(level, msg) / log(msg)) and math libraries are overload sets.

Practice

Q1. (Easy) Can two C++ functions differ only in return type?

Answer: No — return type is not part of overload resolution for ordinary functions.

Q2. (Medium) Which overload does print(0) pick if only print(int) and print(const char*) exist? And print(nullptr)?

Answer: print(0) → int (exact match). print(nullptr) → const char* (nullptr_t converts to pointers, not to int).

Q3. (Advanced) Replace the ambiguous scale(float) / scale(double) pair with a single constrained template that accepts any arithmetic type.

#include <concepts>
#include <iostream>
template <typename T>
    requires std::integral<T> || std::floating_point<T>
double scale(T value, double factor = 1.5) { return static_cast<double>(value) * factor; }
int main() {
    std::cout << scale(10) << " " << scale(2.0f) << " " << scale(4.0, 0.5) << "\n";
}

Interview Question

Q1. "What's the difference between overloading and overriding?"

Answer: Overloading = same name, different parameter lists, chosen at compile time from argument types, within one scope. Overriding = a derived class replaces a virtual function with the same signature, chosen at run time from the object's dynamic type. A derived function with the same name but different parameters *hides* the base functions rather than overriding them (Module 7).

Quick Quiz

Q1. Which ranks higher: promotion or user-defined conversion?

Answer: Promotion.

Q2. How does Python emulate overloading?

Answer: Default/keyword arguments, *args, type checks, or functools.singledispatch.

Q3. What do TypeScript overload signatures produce at run time?

Answer: One implementation function; the signatures are type-only.

Summary

Overload resolution ranks conversions; ties are ambiguity errors.

Return type alone doesn't overload; static argument types decide.

Keep overload sets small and semantically identical; prefer constrained templates or named functions.

Related: Module 8, Module 16 (operators), Module 17 (templates).