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
#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
// 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 (...)#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
#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:
#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
#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
#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++filtThis is why C++ functions cannot be called from C without extern "C" (which disables mangling):
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
constand non-constmember 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.