C++ Operator Overloading: Complete Guide with Examples
What Is Operator Overloading?
C++ lets you redefine how operators behave for your custom types. When you write a + b where a and b are integers, the compiler knows what to do. But what if they are Vector2D objects? Operator overloading lets you teach the compiler how to add vectors, compare dates, or print your objects with cout.
This is one of the features that makes C++ uniquely expressive. You can write vec1 + vec2 instead of vec1.add(vec2), making code read almost like mathematics. But with great power comes responsibility — overloading should make your code more intuitive, not less. If someone reads a + b, they should immediately understand what it does.
Under the hood, a + b is just syntactic sugar for a function call. The compiler translates it to either a.operator+(b) (member function) or operator+(a, b) (free function). This means overloading an operator is simply defining a function with a special name.
Basic Syntax
Here is the simplest example — a Point struct with an overloaded + operator:
#include <iostream>
using namespace std;
struct Point {
double x, y;
// Overload + as a member function
Point operator+(const Point& other) const {
return {x + other.x, y + other.y};
}
};
int main() {
Point a{1.0, 2.0};
Point b{3.0, 4.0};
Point c = a + b; // calls a.operator+(b)
cout << "(" << c.x << ", " << c.y << ")" << endl;
// Output: (4, 6)
return 0;
}
The function name is operator+. It takes the right-hand operand as a parameter (the left-hand operand is *this). The const at the end means calling + does not modify the left operand — it returns a new Point.
Arithmetic Operators (+, -, *, /)
Arithmetic operators should return a new object (by value), not modify the operands. Here is a complete Vector2D class with all four basic arithmetic operators:
#include <iostream>
#include <cmath>
using namespace std;
class Vector2D {
double x, y;
public:
Vector2D(double x = 0, double y = 0) : x(x), y(y) {}
// Arithmetic operators
Vector2D operator+(const Vector2D& v) const { return {x + v.x, y + v.y}; }
Vector2D operator-(const Vector2D& v) const { return {x - v.x, y - v.y}; }
// Scalar multiplication
Vector2D operator*(double scalar) const { return {x * scalar, y * scalar}; }
Vector2D operator/(double scalar) const {
if (scalar == 0) throw runtime_error("Division by zero");
return {x / scalar, y / scalar};
}
// Compound assignment — modifies *this and returns reference
Vector2D& operator+=(const Vector2D& v) {
x += v.x;
y += v.y;
return *this;
}
double magnitude() const { return sqrt(x * x + y * y); }
// Friend for stream output
friend ostream& operator<<(ostream& os, const Vector2D& v);
};
ostream& operator<<(ostream& os, const Vector2D& v) {
return os << "(" << v.x << ", " << v.y << ")";
}
int main() {
Vector2D a(3, 4), b(1, 2);
cout << "a + b = " << a + b << endl; // (4, 6)
cout << "a - b = " << a - b << endl; // (2, 2)
cout << "a * 2 = " << a * 2 << endl; // (6, 8)
cout << "b / 2 = " << b / 2 << endl; // (0.5, 1)
a += b;
cout << "a += b: " << a << endl; // (4, 6)
return 0;
}
Notice the difference between operator+ (returns a new object by value) and operator+= (modifies *this and returns a reference). A common pattern is to implement + in terms of +=:
Vector2D operator+(const Vector2D& v) const {
Vector2D result = *this; // copy
result += v; // reuse +=
return result;
}
This avoids code duplication and ensures + and += always agree.
Comparison Operators (==, !=, <, >)
Comparison operators return bool. If you define ==, you should also define !=. If you define <, consider defining all six relational operators — or use C++20’s spaceship operator (covered later).
#include <iostream>
#include <string>
using namespace std;
class Date {
int year, month, day;
public:
Date(int y, int m, int d) : year(y), month(m), day(d) {}
bool operator==(const Date& d) const {
return year == d.year && month == d.month && day == d.day;
}
bool operator!=(const Date& d) const {
return !(*this == d); // reuse ==
}
bool operator<(const Date& d) const {
if (year != d.year) return year < d.year;
if (month != d.month) return month < d.month;
return day < d.day;
}
bool operator>(const Date& d) const { return d < *this; }
bool operator<=(const Date& d) const { return !(d < *this); }
bool operator>=(const Date& d) const { return !(*this < d); }
friend ostream& operator<<(ostream& os, const Date& d) {
return os << d.year << "-"
<< (d.month < 10 ? "0" : "") << d.month << "-"
<< (d.day < 10 ? "0" : "") << d.day;
}
};
int main() {
Date today(2026, 6, 12);
Date launch(2026, 1, 15);
cout << boolalpha;
cout << today << " == " << launch << "? " << (today == launch) << endl;
cout << today << " > " << launch << "? " << (today > launch) << endl;
// 2026-06-12 == 2026-01-15? false
// 2026-06-12 > 2026-01-15? true
return 0;
}
The pattern here is important: implement == and < from scratch, then build every other comparison on top of them. This eliminates bugs from inconsistent logic.
Stream Operators (<< and >>)
The stream insertion operator << lets you print your objects with cout. It must be a free function (not a member) because the left operand is ostream, not your class. You typically declare it as a friend so it can access private data:
#include <iostream>
#include <string>
using namespace std;
class Color {
int r, g, b;
public:
Color(int r, int g, int b) : r(r), g(g), b(b) {}
// Output: Color(255, 128, 0)
friend ostream& operator<<(ostream& os, const Color& c) {
return os << "Color(" << c.r << ", " << c.g << ", " << c.b << ")";
}
// Input: reads three ints
friend istream& operator>>(istream& is, Color& c) {
is >> c.r >> c.g >> c.b;
// Validate
if (c.r < 0 || c.r > 255 || c.g < 0 || c.g > 255 || c.b < 0 || c.b > 255) {
is.setstate(ios::failbit);
}
return is;
}
};
int main() {
Color orange(255, 165, 0);
cout << orange << endl; // Color(255, 165, 0)
Color input(0, 0, 0);
cout << "Enter R G B: ";
cin >> input;
if (cin) {
cout << "You entered: " << input << endl;
} else {
cout << "Invalid color values!" << endl;
}
return 0;
}
Key rules: operator<< takes ostream& and returns ostream& so you can chain calls (cout << a << b). The second parameter is const because printing should not modify the object. operator>> takes istream& and a non-const reference (it needs to write to the object).
Subscript Operator []
The subscript operator lets your class behave like an array or map. It must be a member function. You typically provide two overloads — one const (for read access) and one non-const (for write access):
#include <iostream>
#include <stdexcept>
using namespace std;
class IntArray {
int* data;
int size_;
public:
IntArray(int n) : data(new int[n]()), size_(n) {}
~IntArray() { delete[] data; }
int size() const { return size_; }
// Non-const: allows modification
int& operator[](int index) {
if (index < 0 || index >= size_)
throw out_of_range("Index out of bounds");
return data[index];
}
// Const: read-only access
const int& operator[](int index) const {
if (index < 0 || index >= size_)
throw out_of_range("Index out of bounds");
return data[index];
}
};
int main() {
IntArray arr(5);
arr[0] = 10; // calls non-const version
arr[1] = 20;
arr[2] = 30;
const IntArray& ref = arr;
cout << ref[0] << endl; // calls const version: 10
// ref[0] = 99; // ERROR: const version returns const ref
return 0;
}
The bounds checking in operator[] is optional but recommended. The standard library’s std::vector does not bounds-check [] (for performance) but does check .at().
Assignment Operator =
The copy assignment operator is called when you assign one existing object to another. It is closely related to the copy constructor but has an extra step: cleaning up the old state before copying new data.
#include <iostream>
#include <algorithm>
using namespace std;
class DynString {
char* data;
int len;
public:
DynString(const char* s = "") {
len = strlen(s);
data = new char[len + 1];
strcpy(data, s);
}
// Copy constructor
DynString(const DynString& other) {
len = other.len;
data = new char[len + 1];
strcpy(data, other.data);
}
// Copy assignment (copy-and-swap idiom)
DynString& operator=(DynString other) { // note: pass by value
swap(data, other.data);
swap(len, other.len);
return *this;
// other is destroyed here, freeing old data
}
~DynString() { delete[] data; }
friend ostream& operator<<(ostream& os, const DynString& s) {
return os << s.data;
}
};
int main() {
DynString a("Hello");
DynString b("World");
cout << "a = " << a << endl; // Hello
b = a; // copy assignment
cout << "b = " << b << endl; // Hello
return 0;
}
The copy-and-swap idiom is the safest way to implement operator=. By taking the parameter by value, the copy constructor handles allocation. Then we swap the internals. The old data is freed when the parameter goes out of scope. This is automatically exception-safe and handles self-assignment correctly.
Increment and Decrement (++, –)
C++ distinguishes prefix (++x) from postfix (x++) using a dummy int parameter:
#include <iostream>
using namespace std;
class Counter {
int value;
public:
Counter(int v = 0) : value(v) {}
// Prefix: ++counter (increment, return new value)
Counter& operator++() {
++value;
return *this;
}
// Postfix: counter++ (return old value, then increment)
Counter operator++(int) { // dummy int distinguishes postfix
Counter old = *this;
++value;
return old;
}
// Same pattern for --
Counter& operator--() { --value; return *this; }
Counter operator--(int) { Counter old = *this; --value; return old; }
friend ostream& operator<<(ostream& os, const Counter& c) {
return os << c.value;
}
};
int main() {
Counter c(5);
cout << "c = " << c << endl; // 5
cout << "++c = " << ++c << endl; // 6
cout << "c++ = " << c++ << endl; // 6 (returns old value)
cout << "c = " << c << endl; // 7
return 0;
}
Prefix returns a reference (the modified object). Postfix returns a copy (the old value). This is why ++i is slightly more efficient than i++ — postfix creates a temporary copy.
Member vs Free Functions
Some operators must be members, some must be free, and some can be either. Here is the decision guide:
Must be member functions:
=(assignment)[](subscript)->(member access)()(function call)
Must be free functions:
<<and>>(stream operators) — the left operand is a stream, not your class
Prefer free functions (often as friends):
- Symmetric binary operators:
+,-,*,==,!=,< - Reason: if
+is a member ofVector2D, thenvec * 2.0works but2.0 * vecdoes not (the left operand must be aVector2D). A free function handles both:
// Free function: works for both vec * 2.0 and 2.0 * vec
Vector2D operator*(const Vector2D& v, double s) { return {v.x * s, v.y * s}; }
Vector2D operator*(double s, const Vector2D& v) { return v * s; } // reuse
Prefer member functions:
- Unary operators:
++,--,-(negation),! - Compound assignment:
+=,-=,*=
The Spaceship Operator <=> (C++20)
Before C++20, implementing all six comparison operators meant writing a lot of boilerplate (as we saw with the Date class). C++20 introduces the three-way comparison operator <=> (informally called the spaceship operator). Define it once, and the compiler generates all six comparisons:
#include <iostream>
#include <compare>
using namespace std;
class Version {
int major, minor, patch;
public:
Version(int ma, int mi, int p) : major(ma), minor(mi), patch(p) {}
// One operator replaces all six comparisons
auto operator<=>(const Version&) const = default;
friend ostream& operator<<(ostream& os, const Version& v) {
return os << v.major << "." << v.minor << "." << v.patch;
}
};
int main() {
Version v1(2, 1, 0), v2(2, 3, 1);
cout << boolalpha;
cout << v1 << " < " << v2 << "? " << (v1 < v2) << endl; // true
cout << v1 << " == " << v2 << "? " << (v1 == v2) << endl; // false
cout << v1 << " >= " << v2 << "? " << (v1 >= v2) << endl; // false
return 0;
}
The = default tells the compiler to compare members in declaration order (major first, then minor, then patch) — exactly what we want for version numbers. For custom logic, implement the body yourself and return std::strong_ordering, std::weak_ordering, or std::partial_ordering.
Rules and Best Practices
Only overload when the meaning is intuitive. Adding two vectors with + makes sense. Using + to delete something from a container does not. If a reader cannot guess what an operator does without looking at the implementation, use a named function instead.
Overload related operators together. If you define ==, define !=. If you define +, define +=. Inconsistent operator sets are a common source of bugs.
Preserve the semantics of built-in operators. + should not modify its operands. = should return *this by reference. ++ prefix should return a reference, postfix a copy. Following these conventions means your types work correctly with generic code.
You cannot overload these operators: :: (scope resolution), . (member access), .* (member pointer access), ?: (ternary), sizeof, typeid, and the cast operators (static_cast, etc.).
You cannot create new operators. You can only overload existing C++ operators. There is no operator** for exponentiation.
You cannot change precedence or associativity. * always binds tighter than +, regardless of what you overload them to do.
Real-World Example: Matrix Class
#include <iostream>
#include <vector>
#include <stdexcept>
#include <iomanip>
using namespace std;
class Matrix {
vector<vector<double>> data;
int rows_, cols_;
public:
Matrix(int r, int c, double val = 0)
: data(r, vector<double>(c, val)), rows_(r), cols_(c) {}
int rows() const { return rows_; }
int cols() const { return cols_; }
// Subscript: mat[i][j] — returns a row
vector<double>& operator[](int i) { return data[i]; }
const vector<double>& operator[](int i) const { return data[i]; }
// Addition
Matrix operator+(const Matrix& m) const {
if (rows_ != m.rows_ || cols_ != m.cols_)
throw invalid_argument("Matrix size mismatch");
Matrix result(rows_, cols_);
for (int i = 0; i < rows_; i++)
for (int j = 0; j < cols_; j++)
result[i][j] = data[i][j] + m[i][j];
return result;
}
// Matrix multiplication
Matrix operator*(const Matrix& m) const {
if (cols_ != m.rows_)
throw invalid_argument("Cannot multiply: column/row mismatch");
Matrix result(rows_, m.cols_);
for (int i = 0; i < rows_; i++)
for (int j = 0; j < m.cols_; j++)
for (int k = 0; k < cols_; k++)
result[i][j] += data[i][k] * m[k][j];
return result;
}
// Scalar multiplication
Matrix operator*(double s) const {
Matrix result(rows_, cols_);
for (int i = 0; i < rows_; i++)
for (int j = 0; j < cols_; j++)
result[i][j] = data[i][j] * s;
return result;
}
// Equality
bool operator==(const Matrix& m) const {
if (rows_ != m.rows_ || cols_ != m.cols_) return false;
for (int i = 0; i < rows_; i++)
for (int j = 0; j < cols_; j++)
if (data[i][j] != m[i][j]) return false;
return true;
}
// Stream output
friend ostream& operator<<(ostream& os, const Matrix& m) {
for (int i = 0; i < m.rows_; i++) {
os << "| ";
for (int j = 0; j < m.cols_; j++)
os << setw(6) << m[i][j] << " ";
os << "|" << endl;
}
return os;
}
};
int main() {
Matrix a(2, 2);
a[0][0] = 1; a[0][1] = 2;
a[1][0] = 3; a[1][1] = 4;
Matrix b(2, 2);
b[0][0] = 5; b[0][1] = 6;
b[1][0] = 7; b[1][1] = 8;
cout << "A + B:" << endl << a + b << endl;
cout << "A * B:" << endl << a * b << endl;
cout << "A * 10:" << endl << a * 10 << endl;
return 0;
}
This Matrix class demonstrates multiple overloaded operators working together. The [] operator returns a row vector, so mat[i][j] uses the built-in vector::operator[] for the second index. This is a common pattern for 2D containers.
Practice Exercises
Exercise 1: Create a Fraction class that overloads +, -, *, /, ==, <, and <<. Fractions should auto-simplify using GCD. Test with Fraction(1,2) + Fraction(1,3) → 5/6.
Exercise 2: Create a BigInt class that stores digits in a vector and overloads +, ==, <, and << to handle numbers larger than long long.
Exercise 3: Create a SmartArray class that overloads [] with bounds checking, + to concatenate arrays, and == to compare element-by-element.
Exercise 4: Add a <=> operator to the Date class from this lesson (requires C++20). Verify that all six comparison operators work with only that one definition.
Summary
Operator overloading lets your classes integrate seamlessly with C++ syntax. You learned how to overload arithmetic operators, comparisons, stream operators, subscript, assignment, and increment/decrement. The key takeaways: use the const qualifier correctly, return types matter (value vs reference), prefer free functions for symmetric operators, and never overload an operator if the meaning is not immediately obvious. The C++20 spaceship operator eliminates comparison boilerplate entirely. In the next lesson, you will learn about enumerations — a way to define named constants that make your code safer and more readable.