C++ Function Overloading
What is Function Overloading?
Function overloading allows you to create multiple functions with the same name but different parameters in C++. The compiler determines which function to call based on the arguments passed.
This enables you to use the same function name for similar operations that work with different data types.
Basic Syntax
#include <string>
using std::string;
// Overloaded functions example
int add(int a, int b) {
return a + b;
}
double add(double a, double b) {
return a + b;
}
string add(const string& a, const string& b) {
return a + b;
}
Key Rules
- Overloaded functions must differ in their parameter lists (number, type, or order of parameters).
- Return type alone is not sufficient to overload a function.
- Functions can be overloaded in the same scope (namespace or class). A derived class can also expose base-class overloads with a `using Base::name;` declaration.
Valid Overloading | Invalid Overloading |
---|---|
Different parameter types (including reference vs const reference, pointer vs pointer-to-const) | Different return types only |
Different number of parameters | Different parameter names only |
Different parameter order (when types differ) | Different default arguments only (defaults are not part of the signature) |
Different cv/ref qualifiers on member functions (e.g., `int f() const;` vs `int f();`, or `void g() &;` vs `void g() &&;`) | Different exception specification / `noexcept` only |
Note: top-level const on a **by-value** parameter does NOT make a new overload (e.g., `int f(int)` vs `int f(const int)` is the same) | — |
Practical Example
#include <iostream>
using namespace std;
// Volume of a cube
float volume(float side) {
return side * side * side;
}
// Volume of a cylinder
float volume(float radius, float height) {
return 3.14159f * radius * radius * height;
}
// Volume of a rectangular prism
float volume(float length, float width, float height) {
return length * width * height;
}
int main() {
cout << "Cube volume: " << volume(5.0f) << '\n';
cout << "Cylinder volume: " << volume(3.0f, 10.0f) << '\n';
cout << "Rectangular prism volume: " << volume(4.0f, 6.0f, 2.0f) << '\n';
return 0;
}
Cube volume: 125 Cylinder volume: 282.743 Rectangular prism volume: 48
Const, References, and Member Overloads
Overloads can differ by reference/pointer constness (e.g., `void f(int&)` vs `void f(const int&)`, `void f(int*)` vs `void f(const int*)`).
Top-level const on a by-value parameter does NOT create a distinct overload (e.g., `int f(int)` and `int f(const int)` are the same).
For member functions, `const`/`volatile` qualifiers and ref-qualifiers (`&`, `&&`) participate in overloading (e.g., `int get() const;` vs `int get();`, `void use() &;` vs `void use() &&;`).
struct S {
int get() const { return 1; }
int get() { return 2; }
void use() & { /* lvalue object */ }
void use() && { /* rvalue object */ }
};
Common Pitfalls
- Ambiguous calls when the compiler can't determine which overload to use (e.g., passing a literal or `nullptr` that matches multiple candidates).
- Hidden overloads when derived class functions hide base class functions with the same name; fix by adding `using Base::name;` in the derived class to bring base overloads into the overload set.
- Performance impact from implicit conversions when calling overloaded functions; prefer exact matches, or use templates to avoid unnecessary conversions.
Best Practices
1. Use overloading for functions that perform conceptually similar operations.
2. Keep overloads consistent in behavior and semantics.
3. Document all overloads clearly.
4. Prefer templates when the logic is identical across types.
5. Avoid overload sets that differ only subtly (which encourage implicit conversions or ambiguity).