Contact
Evil Genius SocietyHomeLearnForumLeaderboardSocialsMessages
← Back to module

Lesson 6 of 8

C++ lets several functions share a name as long as their parameters differ:

#include <iostream>
#include <string>

void print(int value) {
    std::cout << "int: " << value << "\n";
}

void print(double value) {
    std::cout << "double: " << value << "\n";
}

void print(const std::string& value) {
    std::cout << "string: " << value << "\n";
}

int main() {
    print(42);
    print(3.14);
    print(std::string("hello"));
    return 0;
}
int: 42
double: 3.14
string: hello

Three separate functions. The compiler picks one by looking at the argument types — a process called overload resolution, done entirely at compile time with no run-time cost.

Without overloading you'd need print_int, print_double, print_string, and callers would have to remember which. This is why std::cout << works with everything: operator<< is overloaded dozens of times.

What counts as different

Overloads must differ in the number or types of parameters:

void f(int);
void f(int, int);          // different count — fine
void f(double);            // different type — fine
void f(const std::string&); // fine

Return type alone is not enough:

// WILL NOT COMPILE
int value();
double value();

error: ambiguating new declaration of 'double value()'

That seems arbitrary until you consider value(); as a statement, discarding the result — the compiler would have no information to choose from. Overload resolution looks only at arguments.

How the compiler chooses

For each candidate it ranks the conversions needed, roughly best to worst:

  1. Exact match — no conversion at all.
  2. Promotion — float→double, char→int. Cheap and lossless.
  3. Standard conversion — int→double, double→int, anything→bool.
  4. User-defined conversion — via a constructor or conversion operator (Module 9).

The best available match wins. A tie is a compile error, not a coin flip:

// WILL NOT COMPILE
#include <iostream>

void f(long) { std::cout << "long\n"; }
void f(double) { std::cout << "double\n"; }

int main() {
    f(3);         // int -> long and int -> double are both rank-3 conversions
    return 0;
}

error: call of overloaded 'f(int)' is ambiguous

C++ refuses to guess. Fix it by being explicit — f(3.0) or f(static_cast<long>(3)) — or by adding an int overload that matches exactly.

Note how finely this is graded. Swap long for int in that example and it compiles fine: f(3) then matches f(int) exactly, which beats everything. And with f(int)/f(double), calling f(3.0f) is also fine — float→double is a promotion (rank 2), which outranks float→int (rank 3). Ambiguity needs two candidates at the same rank.

The surprise: literals

#include <iostream>

void show(int value) {
    std::cout << "int " << value << "\n";
}

void show(char value) {
    std::cout << "char " << value << "\n";
}

int main() {
    show(65);
    show('A');
    return 0;
}
int 65
char A

65 and 'A' are the same number (lesson 3.5), but their types differ, so they select different overloads. Type, not value, drives the decision.

A related trap: a string literal is const char*, not std::string. Given overloads for bool and std::string, f("hello") may surprisingly pick bool — because pointer→bool is a standard conversion while const char*→std::string is a user-defined one, which ranks worse. If you have both, add a const char* overload.

Overloading and const

A const& parameter and a & parameter are different enough to overload on:

#include <iostream>
#include <string>

void handle(std::string& text) {
    text += " (modified)";
    std::cout << "non-const: " << text << "\n";
}

void handle(const std::string& text) {
    std::cout << "const: " << text << "\n";
}

int main() {
    std::string mutable_text = "hello";
    const std::string fixed_text = "world";

    handle(mutable_text);
    handle(fixed_text);
    handle("temporary");
    return 0;
}
non-const: hello (modified)
const: world
const: temporary

A modifiable object prefers the non-const overload; a const object or a temporary can only bind to the const& one. This pattern shows up throughout the standard library.

When not to overload

Overloading is for functions that do the same thing to different types. If the behaviour genuinely differs, separate names are clearer:

// bad — same name, unrelated work
void process(const Order&);      // charges a card
void process(const LogEntry&);   // writes to disk

// better
void chargeOrder(const Order&);
void writeLogEntry(const LogEntry&);

The reader shouldn't have to check argument types to find out what a call does.

Also note: if the overloads' bodies would be identical apart from the type, you want a template (Module 12), not a pile of copies.

Try it

Write three overloads of area: one taking a radius (double) for a circle, one taking width and height (two doubles) for a rectangle, and one taking a single int side for a square. Call all three and predict which is chosen for area(5).

Checkpoint

  • Overloads share a name and differ in parameter count or types.
  • Return type alone cannot distinguish overloads.
  • Resolution ranks conversions: exact match, promotion, standard conversion, user-defined.
  • An ambiguous call is a compile error — C++ never guesses.
  • Type drives selection, not value: show(65) and show('A') differ.
  • Overload for the same operation on different types; use distinct names when behaviour differs, and templates when the bodies would be identical.

Questions about this lesson

Log in to ask a question.