Operator Overloading
C++ lets you redefine the behaviour of built-in operators (+, -, *, ==, <<, [], etc.) for your own types. Done well, overloading makes custom types feel as natural as built-in types. Done poorly, it creates confusing code. The guiding principle: only overload when the meaning is obvious and matches the conventional use of the operator.
Basic Arithmetic Operators
#include <iostream>
using namespace std;
class Vector2D {
public:
double x, y;
Vector2D(double x = 0, double y = 0) : x(x), y(y) {}
// Member operator: left operand is *this
Vector2D operator+(const Vector2D& rhs) const {
return {x + rhs.x, y + rhs.y};
}
Vector2D operator-(const Vector2D& rhs) const {
return {x - rhs.x, y - rhs.y};
}
Vector2D operator*(double scalar) const {
return {x * scalar, y * scalar};
}
Vector2D& operator+=(const Vector2D& rhs) {
x += rhs.x; y += rhs.y;
return *this;
}
double magnitude() const {
return sqrt(x * x + y * y);
}
};
// Non-member: allows scalar * vector (not just vector * scalar)
Vector2D operator*(double scalar, const Vector2D& v) {
return {v.x * scalar, v.y * scalar};
}
int main() {
Vector2D a(1.0, 2.0), b(3.0, 4.0);
Vector2D sum = a + b;
Vector2D diff = b - a;
Vector2D scaled = a * 3.0;
Vector2D scaled2 = 3.0 * a; // Uses non-member overload
cout << "Sum: (" << sum.x << ", " << sum.y << ")\n"; // (4, 6)
cout << "Scaled: (" << scaled.x << ", " << scaled.y << ")\n"; // (3, 6)
cout << "|b| = " << b.magnitude() << "\n"; // 5
return 0;
}Comparison Operators
#include <iostream>
using namespace std;
class Fraction {
public:
int num, den;
Fraction(int n, int d) : num(n), den(d) {
if (den < 0) { num = -num; den = -den; } // Normalize sign
}
bool operator==(const Fraction& rhs) const {
return num * rhs.den == rhs.num * den;
}
bool operator!=(const Fraction& rhs) const {
return !(*this == rhs);
}
bool operator<(const Fraction& rhs) const {
return num * rhs.den < rhs.num * den;
}
bool operator>(const Fraction& rhs) const {
return rhs < *this;
}
bool operator<=(const Fraction& rhs) const {
return !(rhs < *this);
}
bool operator>=(const Fraction& rhs) const {
return !(*this < rhs);
}
};
int main() {
Fraction a(1, 2), b(2, 4), c(3, 4);
cout << (a == b) << "\n"; // 1 (true — 1/2 == 2/4)
cout << (a < c) << "\n"; // 1 (true — 1/2 < 3/4)
cout << (c > a) << "\n"; // 1 (true)
return 0;
}Note: In C++20, defining operator<=> (the spaceship operator) automatically generates all six comparison operators.
Stream Operators (<< and >>)
These must be non-member functions (the left operand is ostream, not your class):
#include <iostream>
#include <sstream>
using namespace std;
class Complex {
public:
double real, imag;
Complex(double r = 0, double i = 0) : real(r), imag(i) {}
};
// Non-member — left operand is ostream
ostream& operator<<(ostream& os, const Complex& c) {
os << c.real;
if (c.imag >= 0) os << " + " << c.imag << "i";
else os << " - " << (-c.imag) << "i";
return os; // Return os to allow chaining: cout << a << b
}
// Non-member — reads into object
istream& operator>>(istream& is, Complex& c) {
is >> c.real >> c.imag;
return is;
}
int main() {
Complex c1(3.0, 4.0), c2(1.0, -2.0);
cout << c1 << "\n"; // 3 + 4i
cout << c2 << "\n"; // 1 - 2i
// Works with stringstream too
ostringstream oss;
oss << c1;
cout << oss.str() << "\n"; // 3 + 4i
Complex c3;
istringstream iss("5.0 -3.0");
iss >> c3;
cout << c3 << "\n"; // 5 - 3i
return 0;
}Subscript Operator ([])
#include <iostream>
#include <stdexcept>
using namespace std;
class SafeArray {
int data[100];
size_t size_;
public:
SafeArray(size_t size) : size_(size) {
for (size_t i = 0; i < size_; i++) data[i] = 0;
}
// Non-const version — allows modification
int& operator[](size_t index) {
if (index >= size_) throw out_of_range("Index out of bounds");
return data[index];
}
// Const version — called on const objects
const int& operator[](size_t index) const {
if (index >= size_) throw out_of_range("Index out of bounds");
return data[index];
}
size_t size() const { return size_; }
};
int main() {
SafeArray arr(5);
arr[0] = 10;
arr[1] = 20;
arr[2] = 30;
for (size_t i = 0; i < arr.size(); i++) {
cout << arr[i] << " ";
}
cout << "\n"; // 10 20 30 0 0
try {
arr[10] = 99; // Throws!
} catch (const out_of_range& e) {
cout << "Error: " << e.what() << "\n";
}
return 0;
}Function Call Operator (())
A class with operator() is called a functor — an object that behaves like a function:
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
// Functor that multiplies by a factor stored as state
class Multiplier {
double factor;
public:
Multiplier(double f) : factor(f) {}
double operator()(double x) const {
return x * factor;
}
};
// Functor for range-based filtering
class InRange {
int min_, max_;
public:
InRange(int min, int max) : min_(min), max_(max) {}
bool operator()(int x) const { return x >= min_ && x <= max_; }
};
int main() {
Multiplier double_it(2.0);
Multiplier triple(3.0);
cout << double_it(5.0) << "\n"; // 10
cout << triple(5.0) << "\n"; // 15
vector<int> v = {1, 5, 3, 8, 2, 7, 4, 9, 6};
// Count numbers in range 3-7
int count = count_if(v.begin(), v.end(), InRange(3, 7));
cout << "In [3,7]: " << count << "\n"; // 5
// Transform with functor
vector<double> nums = {1.0, 2.0, 3.0, 4.0};
transform(nums.begin(), nums.end(), nums.begin(), double_it);
for (double n : nums) cout << n << " ";
cout << "\n"; // 2 4 6 8
return 0;
}Assignment Operator (=)
#include <iostream>
#include <cstring>
using namespace std;
class String {
char* data;
size_t len;
public:
String(const char* s = "") : len(strlen(s)), data(new char[len + 1]) {
strcpy(data, s);
}
// Copy assignment — copy-and-swap idiom
String& operator=(String other) { // Note: pass by VALUE (copies)
swap(data, other.data); // Swap with the copy
swap(len, other.len);
return *this; // other (old data) destroyed on exit
}
~String() { delete[] data; }
const char* c_str() const { return data; }
};
int main() {
String a("hello");
String b = a; // Copy constructor
String c("world");
c = a; // Copy assignment (operator=)
cout << b.c_str() << "\n"; // hello
cout << c.c_str() << "\n"; // hello
return 0;
}Conversion Operators
#include <iostream>
using namespace std;
class Celsius {
double temp;
public:
Celsius(double t) : temp(t) {}
// Implicit conversion to double (use with care)
operator double() const { return temp; }
// Explicit conversion to bool (safe — only called with explicit cast)
explicit operator bool() const { return temp > 0; }
};
int main() {
Celsius c(25.0);
double t = c; // Implicit conversion
cout << t << "\n"; // 25
if (static_cast<bool>(c)) { // Explicit conversion
cout << "Positive temp\n";
}
return 0;
}Practical Example: Matrix Class
#include <iostream>
#include <vector>
#include <iomanip>
using namespace std;
class Matrix {
int rows, cols;
vector<vector<double>> data;
public:
Matrix(int r, int c, double init = 0.0)
: rows(r), cols(c), data(r, vector<double>(c, init)) {}
double& operator()(int r, int c) { return data[r][c]; }
const double& operator()(int r, int c) const { return data[r][c]; }
Matrix operator+(const Matrix& rhs) const {
Matrix result(rows, cols);
for (int r = 0; r < rows; r++)
for (int c = 0; c < cols; c++)
result(r, c) = data[r][c] + rhs.data[r][c];
return result;
}
Matrix operator*(const Matrix& rhs) const {
Matrix result(rows, rhs.cols);
for (int r = 0; r < rows; r++)
for (int c = 0; c < rhs.cols; c++)
for (int k = 0; k < cols; k++)
result(r, c) += data[r][k] * rhs.data[k][c];
return result;
}
bool operator==(const Matrix& rhs) const {
return data == rhs.data;
}
friend ostream& operator<<(ostream& os, const Matrix& m) {
for (int r = 0; r < m.rows; r++) {
os << "[ ";
for (int c = 0; c < m.cols; c++) {
os << setw(6) << fixed << setprecision(1) << m.data[r][c];
}
os << " ]\n";
}
return os;
}
};
int main() {
Matrix a(2, 2), b(2, 2);
a(0,0) = 1; a(0,1) = 2;
a(1,0) = 3; a(1,1) = 4;
b(0,0) = 5; b(0,1) = 6;
b(1,0) = 7; b(1,1) = 8;
cout << "A:\n" << a;
cout << "B:\n" << b;
cout << "A + B:\n" << (a + b);
cout << "A * B:\n" << (a * b);
// A * B:
// [ 19.0 22.0 ]
// [ 43.0 50.0 ]
return 0;
}Summary
- Overload operators to make custom types feel as natural as built-in types — follow conventions (e.g.,
+should be symmetric and non-mutating) - Binary arithmetic operators (
+,-,*) are best implemented as non-member functions orconstmember functions - Compound assignment operators (
+=,-=) must be member functions — they modify*this - Stream operators (
<<,>>) must be non-member functions (first operand isostream/istream) operator[]should provide both const and non-const overloads- Functors (classes with
operator()) are stateful — they outperform stateless functions when the operation needs captured state - The copy-and-swap idiom in
operator=is exception-safe and handles self-assignment automatically - Never overload
&&,||, or,— they lose their short-circuit/sequencing properties when overloaded
Next, we'll dive into memory management — pointers, references, dynamic allocation, and the tools C++ gives you to do it safely.