Functions

Function Overloading

Understand C++ function overloading — defining multiple functions with the same name, overload resolution rules, and operator overloading basics.

Function Overloading

C++ allows multiple functions with the same name as long as they differ in their parameter types or count. The compiler selects the best matching overload at compile time — this is called overload resolution. Overloading makes APIs feel natural: print(42), print(3.14), and print("hello") all just work.


Basic Overloading

cpp
#include <iostream>
using namespace std;

// Three functions named 'describe', different parameter types
void describe(int n) {
    cout << "Integer: " << n << "\n";
}

void describe(double d) {
    cout << "Double: " << d << "\n";
}

void describe(const string& s) {
    cout << "String: \"" << s << "\"\n";
}

void describe(int a, int b) {
    cout << "Two integers: " << a << " and " << b << "\n";
}

int main() {
    describe(42);             // Integer: 42
    describe(3.14);           // Double: 3.14
    describe("Hello");        // String: "Hello"
    describe(10, 20);         // Two integers: 10 and 20

    return 0;
}

The compiler matches the call to the function with the best-fitting signature. If no exact match exists, it tries implicit conversions.


Rules for Valid Overloading

Functions are distinct overloads when they differ in:

  • Number of parameters
  • Types of parameters
  • Const-ness of reference parameters
cpp
// VALID overloads
int add(int a, int b);         // 2 int parameters
double add(double a, double b);// 2 double parameters
int add(int a, int b, int c);  // 3 int parameters

// INVALID — return type alone does NOT distinguish overloads
int  getValue();   // ERROR if...
double getValue(); // ...this exists too (same name + same params)

Overload Resolution

The compiler follows a hierarchy to find the best match:

1. Exact match (no conversion needed)
2. Trivial conversion (array → pointer, function → pointer)
3. Promotion (char/short → int, float → double)
4. Standard conversion (int → double, int → long, etc.)
5. User-defined conversion (via constructors or cast operators)
6. Variadic (...)
cpp
#include <iostream>
using namespace std;

void show(int x)    { cout << "int:    " << x << "\n"; }
void show(double x) { cout << "double: " << x << "\n"; }
void show(long x)   { cout << "long:   " << x << "\n"; }

int main() {
    show(42);       // int:    42   (exact match)
    show(3.14);     // double: 3.14 (exact match)
    show(42L);      // long:   42   (exact match)
    show('A');      // int:    65   (char promoted to int)
    show(42.0f);    // double: 42   (float promoted to double)

    // AMBIGUOUS — cannot choose between int and double for long
    // show(42LL);  // ERROR: ambiguous (could convert to int or long or double)

    return 0;
}

Overloading with Different Parameter Counts

cpp
#include <iostream>
using namespace std;

// Overload for logging with different levels of context
void log(const string& message) {
    cout << "[LOG] " << message << "\n";
}

void log(const string& category, const string& message) {
    cout << "[" << category << "] " << message << "\n";
}

void log(const string& category, const string& message, int severity) {
    string prefix = (severity >= 3) ? "!!!" : (severity >= 2) ? "!!" : "!";
    cout << prefix << "[" << category << "] " << message << "\n";
}

int main() {
    log("Server started");                         // [LOG] Server started
    log("AUTH", "User logged in");                 // [AUTH] User logged in
    log("AUTH", "Failed login attempt", 3);        // !!![AUTH] Failed login attempt

    return 0;
}

Overloading vs Default Parameters

Both can reduce repetition, but they work differently:

cpp
#include <iostream>
using namespace std;

// Default parameters — ONE function, some params optional
void connect(const string& host,
             int port = 443,
             bool useSSL = true) {
    cout << "Connecting to " << host << ":" << port
         << (useSSL ? " (SSL)" : "") << "\n";
}

// Overloads — SEPARATE functions, different behaviour
void render(int width, int height) {
    cout << "Rendering " << width << "x" << height << "\n";
}
void render(int squareSize) {
    cout << "Rendering " << squareSize << "x" << squareSize << "\n";
}
// PROBLEM: render(100) is ambiguous — compiler can't tell which overload!

int main() {
    connect("api.example.com");              // Uses defaults
    connect("db.example.com", 5432, false);  // All explicit
    return 0;
}

Note: Use default parameters when the missing arguments have a natural default value. Use overloads when the logic genuinely differs between parameter combinations (e.g., different algorithms for different types).


Overloading with const

cpp
#include <iostream>
#include <vector>
using namespace std;

class DataSet {
    vector<int> data;
public:
    DataSet(vector<int> d) : data(d) {}

    // Non-const version — allows modification
    int& at(int index) {
        return data[index];
    }

    // Const version — called on const objects; returns const ref
    const int& at(int index) const {
        return data[index];
    }
};

int main() {
    DataSet ds({10, 20, 30});

    ds.at(0) = 99;    // Calls non-const overload
    cout << ds.at(0) << "\n";   // 99

    const DataSet cds({1, 2, 3});
    // cds.at(0) = 99;  // ERROR — const overload returns const ref
    cout << cds.at(0) << "\n";  // 1

    return 0;
}

Practical Example: Generic Print Function

cpp
#include <iostream>
#include <vector>
#include <map>
#include <string>
using namespace std;

void print(int n)              { cout << n; }
void print(double d)           { cout << d; }
void print(bool b)             { cout << (b ? "true" : "false"); }
void print(const string& s)    { cout << "\"" << s << "\""; }
void print(const char* s)      { cout << "\"" << s << "\""; }

void print(const vector<int>& v) {
    cout << "[";
    for (size_t i = 0; i < v.size(); i++) {
        cout << v[i];
        if (i + 1 < v.size()) cout << ", ";
    }
    cout << "]";
}

void print(const map<string, int>& m) {
    cout << "{";
    bool first = true;
    for (const auto& [key, val] : m) {
        if (!first) cout << ", ";
        cout << "\"" << key << "\": " << val;
        first = false;
    }
    cout << "}";
}

// println wraps print with a newline — works with any overload
template <typename T>
void println(const T& value) {
    print(value);
    cout << "\n";
}

int main() {
    println(42);                                    // 42
    println(3.14);                                  // 3.14
    println(true);                                  // true
    println("Hello");                               // "Hello"
    println(vector<int>{1, 2, 3, 4, 5});            // [1, 2, 3, 4, 5]
    println(map<string,int>{{"a",1},{"b",2}});      // {"a": 1, "b": 2}

    return 0;
}

Name Mangling

The C++ compiler internally renames overloaded functions to embed type information — this process is called name mangling:

// C++ source:
int add(int, int);      → _Z3addii
double add(double, double); → _Z3adddd

// You can see mangled names with:
// nm myprogram | c++filt

This is why C++ functions cannot be called from C without extern "C" (which disables mangling):

cpp
extern "C" {
    int c_compatible_function(int x);  // No mangling — callable from C
}

Summary

  • Overloading lets you define multiple functions with the same name but different parameter types or counts
  • The compiler selects the best matching overload at compile time — this is overload resolution
  • Overloads must differ in parameter types or count — return type alone is not enough
  • Prefer default parameters when optional arguments have natural defaults; prefer overloads when logic genuinely differs
  • const and non-const member functions with the same name are valid overloads
  • Name mangling encodes overload information in the symbol table — use extern "C" for C-compatible symbols
  • Avoid overloads that are ambiguous (the compiler cannot pick one) — add explicit casts to resolve them

Next, we'll explore recursion — functions that call themselves to solve problems that have a self-similar structure.