While operator overloading has existed since early C++, Modern C++ (C++11–C++23) refined how we design, implement, and reason about it — especially with move semantics, constexpr, noexcept, and the spaceship operator.
Why Operator Overloading Exists
C++ is built on the philosophy:
“User-defined types should behave like built-in types.”
Built-in types:
int a = 5 + 3;
double x = y * z;
Custom types should feel the same:
Vector v3 = v1 + v2;
Matrix m = a * b;
Without overloading:
Vector v3 = v1.add(v2);
Overloading improves:
- Expressiveness
- Readability
- Generic programming compatibility
- STL integration
What Is Operator Overloading?
Operator overloading means defining custom behavior for operators when used with user-defined types.
Example:
class Vector {
public:
int x, y;
Vector(int x, int y) : x(x), y(y) {}
Vector operator+(const Vector& other) const {
return Vector(x + other.x, y + other.y);
}
};
Usage:
Vector v1(1,2);
Vector v2(3,4);
Vector v3 = v1 + v2;
The last const means:
This function does NOT modify the current object (`*this`).
How the Compiler Sees It
When you write:
v1 + v2;
The compiler transforms it to:
v1.operator+(v2);
Operators are just special function calls.
Member vs Non-Member Overloading
Member version:
Vector operator+(const Vector& other) const;
Limitation:
- Left operand must be
Vector
Example:
class Rational {
int n {0};
int d {1};
public:
Rational(int numerator = 0, int denominator =1): n(numerator), d(denominator) {}
Rational(const Rational & rhs) : n(rhs.n), d(rhs.d) {} // copy constractor
~Rational();
int numerator() const { return n; };
int denominator() const { return d; };
Rational& operator = (const Rational&) { // assignment
if (this != &rhs) {
n = rhs.numerator();
d = rhs.denominator();
}
return *this;
}
Rational operator + (const Rational&) const {
return Rational((n * rhs.d) + (d * rhs.n), d * rhs.d);
}
Rational operator - (const Rational&) const {
return Rational((n * rhs.d) - (d * rhs.n), d * rhs.d);
}
Rational operator * (const Rational&) const {
return Rational((n * rhs.d) * (d * rhs.n), d * rhs.d);
}
Rational operator / (const Rational&) const {
return Rational((n * rhs.d) + (d * rhs.n), d * rhs.d);
}
};
Usage:
Vector2 a(1, 2);
Vector2 b(3, 4);
Vector2 c = a + b;
Why const matters
- Ensures no mutation
- Enables use with const objects
- Signals intent clearly
Non-member version (recommended for symmetry):
Vector operator+(const Vector& lhs, const Vector& rhs) {
return Vector(lhs.x + rhs.x, lhs.y + rhs.y);
}
This is more flexible and often preferred in Modern C++.
Operators That Can Be Overloaded
Arithmetic: + - * / %
Comparison: == != < > <= >=
Increment / decrement: ++ --
Assignment: = += -= *=
Subscript: []
Function call: ()
Stream: << >>
Operators That MUST Be Members
= [] () ->
Operators That Cannot Be Overloaded
. :: ?: sizeof typeid
**Operators can only be overloaded for user-defined types**
That means:
- ✅ Classes
- ✅ Structs
- ❌ Built-in types (
int,float,double, pointers)
This is intentionally restricted to protect the language from abuse.
❌ Not allowed
int operator+(int a, int b) {
return a - b;
}
✅ Allowed
class Number {
public:
int value;
Number operator+(const Number& other) const {
return Number{value + other.value};
}
};
Examples:
Overloading `operator=`
class Buffer {
public:
int* data;
size_t size;
Buffer(size_t s) : size(s) {
data = new int[s];
}
~Buffer() {
delete[] data;
}
Buffer& operator=(const Buffer& other) {
if (this != &other) {
delete[] data;
size = other.size;
data = new int[size];
std::copy(other.data, other.data + size, data);
}
return *this;
}
};
Important:
- Return reference
- Handle self-assignment
- Deep copy
This connects to:
Rule of Five (Modern C++)
If you define:
- Destructor
- Copy constructor
- Copy assignment
You probably need:
- Move constructor
- Move assignment
Overloading Comparison Operators
Comparison operators allow objects to be compared logically.
Example: Equality
class User {
public:
int id;
bool operator==(const User& other) const {
return id == other.id;
}
};
Usage:
User a{1};
User b{1};
if (a == b) {
// same user
}
Why this matters
- Enables STL algorithms
- Supports containers like
std::set,std::map - Makes domain logic expressive
Comparison Operators (C++20)
C++20 introduced the spaceship operator:
auto operator<=>(const Vector&) const = default;
This generates:
== != < > <= >=
Clean and safe.
Overloading `operator[]`
Used in containers:
int& operator[](size_t index) {
return data[index];
}
const int& operator[](size_t index) const {
return data[index];
}
Two versions:
- Mutable
- Const
The Function Call Operator `operator()`
Now things get interesting.
class Multiplier {
int factor;
public:
Multiplier(int f) : factor(f) {}
int operator()(int x) const {
return x * factor;
}
};
Usage:
Multiplier times3(3);
int result = times3(10); // 30
This object behaves like a function.
These are called Function Objects or Functors.
What Is a Functor?
A functor is:
An object that can be called like a function.
Implemented by overloading: operator()
Example in STL:
std::sort(vec.begin(), vec.end(), std::greater<>());
std::greater<> is a functor.
Why Functors Are Powerful
Compared to normal functions:
- Can store state
- Inlineable
- Zero overhead
- Template-friendly
- Compile-time optimized
Example with state:
class Threshold {
int limit;
public:
Threshold(int l) : limit(l) {}
bool operator()(int x) const {
return x > limit;
}
};
Transition: From Functors to Lambdas
Functors are powerful — but verbose.
Modern C++ introduced lambdas to replace most functors.
Instead of:
struct Add {
int operator()(int a, int b) const {
return a + b;
}
};
We write:
auto add = [](int a, int b) {
return a + b;
};
Lambdas are:
- Anonymous functors
- Inline
- Concise
- Capture variables
- Foundation of modern STL usage