Arithmetic Operators
Arithmetic operator overloading lets your custom classes support mathematical operations like addition, subtraction, multiplication, and division - making them as intuitive as built-in types.
What is Operator Overloading?
Operator overloading is a powerful C++ feature that allows you to redefine how operators work with
your custom classes. Instead of writing point1.add(point2), you can write
point1 + point2 - much more natural and readable! Think of it as teaching C++ how your
objects should behave when someone uses standard operators on them.
When you create a class like Vector2D or Complex, it makes sense to add
two vectors or multiply complex numbers using + and *. Without operator overloading, you'd need verbose
method calls. With it, your code reads like mathematics.
Operator Overloading
Operator overloading allows you to define custom behavior for C++ operators
when applied to user-defined types (classes and structs). The compiler translates
a + b into a.operator+(b) or operator+(a, b).
You can overload most C++ operators including arithmetic (+, -, *, /), comparison (==, <), assignment (=), subscript ([]), function call (()), and stream (<<, >>) operators.
Cannot overload: :: (scope resolution), . (member access),
.* (member pointer access), ?: (ternary), sizeof, typeid
Binary Arithmetic Operators
Binary operators take two operands - the object on the left and the object on the right. The most common binary arithmetic operators are +, -, *, /, and %. You can implement them as member functions or as non-member (friend) functions.
#include <iostream>
class Vector2D {
public:
double x, y;
Vector2D(double x = 0, double y = 0) : x(x), y(y) {}
// Member function: this + other
Vector2D operator+(const Vector2D& other) const {
return Vector2D(x + other.x, y + other.y);
}
// Member function: this - other
Vector2D operator-(const Vector2D& other) const {
return Vector2D(x - other.x, y - other.y);
}
// Scalar multiplication: vector * scalar
Vector2D operator*(double scalar) const {
return Vector2D(x * scalar, y * scalar);
}
void print() const {
std::cout << "(" << x << ", " << y << ")" << std::endl;
}
};
a + b, you don't expect
either a or b to change. The const at the end of each function
signature promises that the method won't modify the object it's called on.
// Non-member function for scalar * vector (order matters!)
Vector2D operator*(double scalar, const Vector2D& vec) {
return vec * scalar; // Reuse the member function
}
int main() {
Vector2D v1(3, 4);
Vector2D v2(1, 2);
Vector2D sum = v1 + v2; // (4, 6)
Vector2D diff = v1 - v2; // (2, 2)
Vector2D scaled1 = v1 * 2; // (6, 8) - vector * scalar
Vector2D scaled2 = 2 * v1; // (6, 8) - scalar * vector
sum.print(); // (4, 6)
diff.print(); // (2, 2)
scaled1.print(); // (6, 8)
scaled2.print(); // (6, 8)
return 0;
}
vector * scalar (member function) and scalar * vector
(non-member function) for complete flexibility. The non-member version handles the case where the
scalar is on the left side of the multiplication. It simply delegates to the member function,
avoiding code duplication.
Unary Operators
Unary operators work on a single operand. Common examples include negation (-), increment (++), and decrement (--). The increment and decrement operators have both prefix (++x) and postfix (x++) forms, which require different signatures.
#include <iostream>
class Counter {
public:
int value;
Counter(int v = 0) : value(v) {}
// Unary minus: -counter
Counter operator-() const {
return Counter(-value);
}
// Prefix increment: ++counter (returns reference)
Counter& operator++() {
++value;
return *this;
}
// Postfix increment: counter++ (returns copy, takes dummy int)
Counter operator++(int) {
Counter temp = *this; // Save current state
++value; // Increment
return temp; // Return old value
}
// Prefix decrement: --counter
Counter& operator--() {
--value;
return *this;
}
// Postfix decrement: counter--
Counter operator--(int) {
Counter temp = *this;
--value;
return temp;
}
};
++++x. The postfix versions (x++, x--) take a dummy int parameter
(never used, just for overload resolution) and return a copy of the original value before
modification. Postfix is slightly less efficient due to the temporary copy.
int main() {
Counter c(5);
Counter neg = -c; // neg.value = -5, c unchanged
std::cout << neg.value << std::endl; // -5
std::cout << (++c).value << std::endl; // 6 (increment, then use)
std::cout << (c++).value << std::endl; // 6 (use, then increment)
std::cout << c.value << std::endl; // 7 (after postfix)
return 0;
}
Compound Assignment Operators
Compound assignment operators like +=, -=, *=, /= combine an operation with assignment. They modify the left operand in place and return a reference to it. A common pattern is to implement the binary operator in terms of the compound assignment operator.
#include <iostream>
class Money {
public:
int dollars;
int cents;
Money(int d = 0, int c = 0) : dollars(d), cents(c) {
normalize();
}
// Compound assignment: +=
Money& operator+=(const Money& other) {
dollars += other.dollars;
cents += other.cents;
normalize();
return *this;
}
// Binary addition implemented using +=
Money operator+(const Money& other) const {
Money result = *this; // Copy this object
result += other; // Use compound assignment
return result;
}
void print() const {
std::cout << "$" << dollars << "."
<< (cents < 10 ? "0" : "") << cents << std::endl;
}
private:
void normalize() {
if (cents >= 100) {
dollars += cents / 100;
cents = cents % 100;
}
}
};
int main() {
Money wallet(10, 50); // $10.50
Money price(3, 75); // $3.75
wallet += price; // $14.25
wallet.print();
Money total = Money(5, 0) + Money(2, 99); // $7.99
total.print();
return 0;
}
operator+ in terms of operator+= is a best practice.
It reduces code duplication and ensures consistency between the two operations. The compound
assignment modifies and returns *this, while the binary operator creates and
returns a new object.
Practice Questions: Arithmetic Operators
Problem: Create a Fraction class that supports addition and subtraction.
Requirements:
- Store numerator and denominator as integers
- Implement operator+ and operator-
- Include a print() method to display as "num/den"
Show Solution
#include <iostream>
class Fraction {
public:
int num, den;
Fraction(int n = 0, int d = 1) : num(n), den(d) {}
Fraction operator+(const Fraction& other) const {
return Fraction(
num * other.den + other.num * den,
den * other.den
);
}
Fraction operator-(const Fraction& other) const {
return Fraction(
num * other.den - other.num * den,
den * other.den
);
}
void print() const {
std::cout << num << "/" << den << std::endl;
}
};
int main() {
Fraction a(1, 2); // 1/2
Fraction b(1, 3); // 1/3
Fraction sum = a + b; // 5/6
Fraction diff = a - b; // 1/6
sum.print(); // 5/6
diff.print(); // 1/6
}
Problem: Implement a Complex number class with full arithmetic support.
Requirements:
- Store real and imaginary parts as doubles
- Implement +, -, * operators
- Implement unary - (negation)
- Include magnitude() method
Show Solution
#include <iostream>
#include <cmath>
class Complex {
public:
double real, imag;
Complex(double r = 0, double i = 0) : real(r), imag(i) {}
Complex operator+(const Complex& other) const {
return Complex(real + other.real, imag + other.imag);
}
Complex operator-(const Complex& other) const {
return Complex(real - other.real, imag - other.imag);
}
Complex operator*(const Complex& other) const {
return Complex(
real * other.real - imag * other.imag,
real * other.imag + imag * other.real
);
}
Complex operator-() const {
return Complex(-real, -imag);
}
double magnitude() const {
return std::sqrt(real * real + imag * imag);
}
void print() const {
std::cout << real << (imag >= 0 ? "+" : "")
<< imag << "i" << std::endl;
}
};
int main() {
Complex a(3, 4); // 3+4i
Complex b(1, -2); // 1-2i
(a + b).print(); // 4+2i
(a * b).print(); // 11-2i
(-a).print(); // -3-4i
std::cout << "Magnitude: " << a.magnitude() << std::endl; // 5
}
Problem: Create a 2x2 matrix class with matrix arithmetic.
Requirements:
- Store 4 elements (a, b, c, d) for [[a,b],[c,d]]
- Implement + and * (matrix multiplication)
- Implement scalar multiplication
- Include determinant() method
Show Solution
#include <iostream>
class Matrix2x2 {
public:
double a, b, c, d; // [[a,b],[c,d]]
Matrix2x2(double a=0, double b=0, double c=0, double d=0)
: a(a), b(b), c(c), d(d) {}
Matrix2x2 operator+(const Matrix2x2& m) const {
return Matrix2x2(a+m.a, b+m.b, c+m.c, d+m.d);
}
Matrix2x2 operator*(const Matrix2x2& m) const {
return Matrix2x2(
a*m.a + b*m.c, a*m.b + b*m.d,
c*m.a + d*m.c, c*m.b + d*m.d
);
}
Matrix2x2 operator*(double scalar) const {
return Matrix2x2(a*scalar, b*scalar, c*scalar, d*scalar);
}
double determinant() const {
return a*d - b*c;
}
void print() const {
std::cout << "[[" << a << ", " << b << "], ["
<< c << ", " << d << "]]" << std::endl;
}
};
int main() {
Matrix2x2 m1(1, 2, 3, 4);
Matrix2x2 m2(5, 6, 7, 8);
(m1 + m2).print(); // [[6, 8], [10, 12]]
(m1 * m2).print(); // [[19, 22], [43, 50]]
(m1 * 2).print(); // [[2, 4], [6, 8]]
std::cout << "Det: " << m1.determinant() << std::endl; // -2
}
Problem: Create a simple BigInt class with prefix and postfix operators.
Requirements:
- Store value as long long
- Implement prefix ++, -- (return reference)
- Implement postfix ++, -- (return copy)
- Implement compound += and -=
Show Solution
#include <iostream>
class BigInt {
public:
long long value;
BigInt(long long v = 0) : value(v) {}
// Prefix increment
BigInt& operator++() {
++value;
return *this;
}
// Postfix increment
BigInt operator++(int) {
BigInt temp = *this;
++value;
return temp;
}
// Prefix decrement
BigInt& operator--() {
--value;
return *this;
}
// Postfix decrement
BigInt operator--(int) {
BigInt temp = *this;
--value;
return temp;
}
BigInt& operator+=(const BigInt& other) {
value += other.value;
return *this;
}
BigInt& operator-=(const BigInt& other) {
value -= other.value;
return *this;
}
};
int main() {
BigInt n(100);
std::cout << (++n).value << std::endl; // 101
std::cout << (n++).value << std::endl; // 101
std::cout << n.value << std::endl; // 102
n += BigInt(8);
std::cout << n.value << std::endl; // 110
}
Comparison Operators
Comparison operators allow your objects to be compared for equality, ordering, and used in sorting algorithms. C++20 introduced the spaceship operator for simplified comparisons.
Equality Operators: == and !=
The equality operators check whether two objects are equal or not equal. A common pattern is to
implement operator== and then define operator!= in terms of it. In C++20,
the compiler can automatically generate operator!= from operator==.
#include <iostream>
#include <string>
class Person {
public:
std::string name;
int age;
Person(const std::string& n, int a) : name(n), age(a) {}
// Equality: compare all relevant fields
bool operator==(const Person& other) const {
return name == other.name && age == other.age;
}
// Inequality: defined in terms of equality
bool operator!=(const Person& other) const {
return !(*this == other);
}
};
int main() {
Person alice("Alice", 30);
Person bob("Bob", 25);
Person alice2("Alice", 30);
std::cout << std::boolalpha;
std::cout << (alice == alice2) << std::endl; // true
std::cout << (alice == bob) << std::endl; // false
std::cout << (alice != bob) << std::endl; // true
return 0;
}
operator==, decide which fields constitute object equality. For a
Person, both name and age matter. For a database record, perhaps only the ID matters. The
const qualifier is essential - comparison should never modify either object.
Relational Operators: <, >, <=, >=
Relational operators define ordering between objects. They're essential for sorting and for using
your objects in ordered containers like std::set or std::map. Typically,
you implement operator< and derive the others from it.
#include <iostream>
#include <string>
#include <vector>
#include <algorithm>
class Student {
public:
std::string name;
double gpa;
Student(const std::string& n, double g) : name(n), gpa(g) {}
// Primary comparison: by GPA (descending for ranking)
bool operator<(const Student& other) const {
return gpa > other.gpa; // Higher GPA = "less than" for ranking
}
bool operator>(const Student& other) const {
return other < *this;
}
bool operator<=(const Student& other) const {
return !(other < *this);
}
bool operator>=(const Student& other) const {
return !(*this < other);
}
bool operator==(const Student& other) const {
return gpa == other.gpa && name == other.name;
}
};
int main() {
std::vector<Student> students = {
{"Alice", 3.8},
{"Bob", 3.9},
{"Charlie", 3.7}
};
// Sort uses operator<
std::sort(students.begin(), students.end());
for (const auto& s : students) {
std::cout << s.name << ": " << s.gpa << std::endl;
}
// Output: Bob: 3.9, Alice: 3.8, Charlie: 3.7
return 0;
}
operator>, operator<=, and operator>=
are all implemented in terms of operator<. This ensures consistency and reduces
bugs. When operator< returns true for "higher GPA," sorting produces descending
order - perfect for ranking students.
C++20 Spaceship Operator: <=>
C++20 introduced the three-way comparison operator <=>, nicknamed the "spaceship
operator." It can generate all six comparison operators from a single definition, dramatically
reducing boilerplate code.
#include <iostream>
#include <compare> // Required for spaceship operator
class Version {
public:
int major, minor, patch;
Version(int maj, int min, int pat)
: major(maj), minor(min), patch(pat) {}
// C++20: One operator generates all comparisons!
auto operator<=>(const Version& other) const = default;
// If you need custom logic:
// std::strong_ordering operator<=>(const Version& other) const {
// if (auto cmp = major <=> other.major; cmp != 0) return cmp;
// if (auto cmp = minor <=> other.minor; cmp != 0) return cmp;
// return patch <=> other.patch;
// }
};
int main() {
Version v1(2, 0, 0);
Version v2(1, 9, 5);
Version v3(2, 0, 0);
std::cout << std::boolalpha;
std::cout << (v1 > v2) << std::endl; // true
std::cout << (v1 == v3) << std::endl; // true
std::cout << (v2 < v1) << std::endl; // true
std::cout << (v1 >= v3) << std::endl; // true
return 0;
}
= default version performs member-wise comparison in declaration order. For
Version, it compares major first, then minor, then patch - exactly what you'd want! The return
type std::strong_ordering indicates that objects are either less than, equal to,
or greater than each other with no other possibilities.
std::strong_ordering (a == b means identical),
std::weak_ordering (a == b means equivalent but not identical), and
std::partial_ordering (some values may be incomparable, like NaN).
Practice Questions: Comparison Operators
Problem: Create a Point class with x, y coordinates and equality operators.
Requirements:
- Store x and y as integers
- Implement operator== and operator!=
- Two points are equal if both coordinates match
Show Solution
#include <iostream>
class Point {
public:
int x, y;
Point(int x = 0, int y = 0) : x(x), y(y) {}
bool operator==(const Point& other) const {
return x == other.x && y == other.y;
}
bool operator!=(const Point& other) const {
return !(*this == other);
}
};
int main() {
Point p1(3, 4);
Point p2(3, 4);
Point p3(5, 6);
std::cout << std::boolalpha;
std::cout << (p1 == p2) << std::endl; // true
std::cout << (p1 != p3) << std::endl; // true
}
Problem: Implement a Date class that can be compared chronologically.
Requirements:
- Store year, month, day
- Implement all six comparison operators
- Dates should sort chronologically
Show Solution
#include <iostream>
class Date {
public:
int year, month, day;
Date(int y, int m, int d) : year(y), month(m), day(d) {}
bool operator<(const Date& other) const {
if (year != other.year) return year < other.year;
if (month != other.month) return month < other.month;
return day < other.day;
}
bool operator>(const Date& other) const { return other < *this; }
bool operator<=(const Date& other) const { return !(other < *this); }
bool operator>=(const Date& other) const { return !(*this < other); }
bool operator==(const Date& other) const {
return year == other.year && month == other.month
&& day == other.day;
}
bool operator!=(const Date& other) const { return !(*this == other); }
};
int main() {
Date d1(2024, 12, 25);
Date d2(2024, 1, 1);
Date d3(2024, 12, 25);
std::cout << std::boolalpha;
std::cout << (d1 > d2) << std::endl; // true
std::cout << (d1 == d3) << std::endl; // true
std::cout << (d2 < d1) << std::endl; // true
}
Problem: Create a Temperature class that compares values regardless of unit.
Requirements:
- Store value and unit (Celsius or Fahrenheit)
- Comparisons should convert to common unit internally
- 30C should equal 86F
Show Solution
#include <iostream>
#include <cmath>
class Temperature {
public:
enum Unit { Celsius, Fahrenheit };
double value;
Unit unit;
Temperature(double v, Unit u) : value(v), unit(u) {}
double toCelsius() const {
if (unit == Celsius) return value;
return (value - 32) * 5.0 / 9.0;
}
bool operator==(const Temperature& other) const {
return std::abs(toCelsius() - other.toCelsius()) < 0.001;
}
bool operator<(const Temperature& other) const {
return toCelsius() < other.toCelsius();
}
bool operator>(const Temperature& other) const {
return other < *this;
}
bool operator!=(const Temperature& other) const {
return !(*this == other);
}
};
int main() {
Temperature t1(30, Temperature::Celsius);
Temperature t2(86, Temperature::Fahrenheit);
Temperature t3(0, Temperature::Celsius);
std::cout << std::boolalpha;
std::cout << (t1 == t2) << std::endl; // true (30C = 86F)
std::cout << (t3 < t1) << std::endl; // true
}
Problem: Create an Employee class that sorts by salary (descending), then by name.
Requirements:
- Store name (string) and salary (double)
- Primary sort: salary descending
- Secondary sort: name ascending
- Test with std::sort
Show Solution
#include <iostream>
#include <vector>
#include <algorithm>
#include <string>
class Employee {
public:
std::string name;
double salary;
Employee(const std::string& n, double s) : name(n), salary(s) {}
bool operator<(const Employee& other) const {
if (salary != other.salary) {
return salary > other.salary; // Descending by salary
}
return name < other.name; // Ascending by name
}
};
int main() {
std::vector<Employee> employees = {
{"Alice", 75000},
{"Bob", 80000},
{"Charlie", 75000},
{"Diana", 90000}
};
std::sort(employees.begin(), employees.end());
for (const auto& e : employees) {
std::cout << e.name << ": $" << e.salary << std::endl;
}
// Diana: $90000
// Bob: $80000
// Alice: $75000
// Charlie: $75000
}
Stream Operators
Stream operators let your objects work seamlessly with iostream for input and output. Overloading << and >> makes debugging and user interaction much more convenient.
Output Operator: <<
The output operator << (insertion operator) sends data to an output stream like
std::cout. It must be a non-member function because the left operand is the stream, not
your class. Making it a friend allows access to private members.
#include <iostream>
#include <string>
class Book {
public:
std::string title;
std::string author;
int year;
Book(const std::string& t, const std::string& a, int y)
: title(t), author(a), year(y) {}
// Friend function for output - NOT a member!
friend std::ostream& operator<<(std::ostream& os, const Book& book) {
os << "\"" << book.title << "\" by " << book.author
<< " (" << book.year << ")";
return os; // Return stream for chaining
}
};
int main() {
Book book1("1984", "George Orwell", 1949);
Book book2("Dune", "Frank Herbert", 1965);
std::cout << book1 << std::endl;
// Output: "1984" by George Orwell (1949)
// Chaining works because we return the stream
std::cout << book1 << " and " << book2 << std::endl;
return 0;
}
std::ostream& (a reference to the stream) to enable chaining
like std::cout << a << b << c. The friend declaration
inside the class grants the non-member function access to private members. The Book parameter
is const because outputting should never modify the object.
Input Operator: >>
The input operator >> (extraction operator) reads data from an input stream like
std::cin. Unlike output, the object being read into must be modifiable, so it's not
const.
#include <iostream>
#include <string>
class Point3D {
public:
double x, y, z;
Point3D(double x = 0, double y = 0, double z = 0)
: x(x), y(y), z(z) {}
// Output operator
friend std::ostream& operator<<(std::ostream& os, const Point3D& p) {
os << "(" << p.x << ", " << p.y << ", " << p.z << ")";
return os;
}
const because displaying a point doesn't change its values. This operator works
with any output stream (cout, file streams, string streams). The parentheses and commas make the
output human-readable and match mathematical notation for coordinates.
// Input operator - object is NOT const (we're modifying it)
friend std::istream& operator>>(std::istream& is, Point3D& p) {
is >> p.x >> p.y >> p.z;
return is;
}
};
>> reads three whitespace-separated values from the stream and stores
them in x, y, and z. The operator returns the stream reference to enable chaining like
cin >> p1 >> p2.
int main() {
Point3D p;
std::cout << "Enter x y z coordinates: ";
std::cin >> p; // User types: 1.5 2.5 3.5
>> operator extracts those values and assigns them to p.x, p.y,
and p.z. The syntax std::cin >> p looks just like reading a built-in type!
std::cout << "You entered: " << p << std::endl;
// Output: You entered: (1.5, 2.5, 3.5)
return 0;
}
<< operator. This demonstrates
how input and output operators work together seamlessly - the user provides raw numbers, and we
display them in a formatted, readable way. The output shows the point in mathematical notation
with parentheses and commas.
is.fail() or is.good(). If the user enters invalid data (like text
instead of numbers), the stream enters a fail state. You can check this condition and handle
errors appropriately:
if (!(std::cin >> p)) {
std::cout << "Invalid input!" << std::endl;
}
Formatted Output
You can create sophisticated formatted output using stream manipulators and formatting. This is especially useful for displaying tables, aligned data, or specific numeric formats.
#include <iostream>
#include <iomanip>
#include <string>
<iomanip> header provides stream manipulators for formatting output.
This includes functions like setw() for setting field width, setprecision()
for decimal precision, and alignment controls. These manipulators work with the stream insertion
operator to create professional-looking formatted output.
class Product {
public:
std::string name;
double price;
int quantity;
Product(const std::string& n, double p, int q)
: name(n), price(p), quantity(q) {}
friend std::ostream& operator<<(std::ostream& os, const Product& p) {
os << std::left << std::setw(20) << p.name
std::left and reserves 20
characters of width with setw(20). This means the product name will be padded
with spaces on the right to fill 20 characters total. Left alignment is perfect for text fields
like names because it keeps the text flush to the left edge while adding padding on the right.
<< std::right << std::setw(10) << std::fixed
<< std::setprecision(2) << "$" << p.price
std::right) so numbers line up at
the decimal point. std::fixed forces fixed-point notation (not scientific), and
setprecision(2) displays exactly 2 decimal places. The width of 10 includes the
dollar sign, number, and padding. Right-aligned numbers create clean columns in tables.
<< std::setw(8) << p.quantity << " units";
return os;
}
};
os) to enable chaining. These manipulators persist - once you set std::right
or std::fixed, they remain active for subsequent insertions on that stream.
int main() {
Product p1("Laptop", 999.99, 50);
Product p2("Mouse", 29.99, 200);
Product p3("Keyboard", 79.50, 150);
std::cout << "Product "
<< " Price Qty" << std::endl;
std::cout << std::string(45, '-') << std::endl;
std::cout << p1 << std::endl;
std::cout << p2 << std::endl;
std::cout << p3 << std::endl;
return 0;
}
std::string(45, '-') (a string of 45 dashes). Each product outputs on its own line,
and the formatting ensures perfect column alignment. The result is a professional-looking table
where product names are left-aligned, prices show exactly 2 decimals and align right, and
quantities form a neat right-aligned column.
Product Price Qty --------------------------------------------- Laptop $999.99 50 units Mouse $29.99 200 units Keyboard $79.50 150 units
Practice Questions: Stream Operators
Problem: Create a Color class with RGB values and stream operators.
Requirements:
- Store red, green, blue as integers (0-255)
- Output format: "RGB(255, 128, 0)"
- Input reads three space-separated integers
Show Solution
#include <iostream>
class Color {
public:
int r, g, b;
Color(int r = 0, int g = 0, int b = 0) : r(r), g(g), b(b) {}
friend std::ostream& operator<<(std::ostream& os, const Color& c) {
os << "RGB(" << c.r << ", " << c.g << ", " << c.b << ")";
return os;
}
friend std::istream& operator>>(std::istream& is, Color& c) {
is >> c.r >> c.g >> c.b;
return is;
}
};
int main() {
Color red(255, 0, 0);
std::cout << red << std::endl; // RGB(255, 0, 0)
Color custom;
std::cout << "Enter R G B: ";
std::cin >> custom; // User enters: 128 64 255
std::cout << custom << std::endl; // RGB(128, 64, 255)
}
Problem: Implement a Time class with hours, minutes, seconds.
Requirements:
- Output format: "HH:MM:SS" with leading zeros
- Input reads time in "H M S" format
- Use <iomanip> for formatting
Show Solution
#include <iostream>
#include <iomanip>
class Time {
public:
int hours, minutes, seconds;
Time(int h = 0, int m = 0, int s = 0)
: hours(h), minutes(m), seconds(s) {}
friend std::ostream& operator<<(std::ostream& os, const Time& t) {
os << std::setfill('0')
<< std::setw(2) << t.hours << ":"
<< std::setw(2) << t.minutes << ":"
<< std::setw(2) << t.seconds;
return os;
}
friend std::istream& operator>>(std::istream& is, Time& t) {
is >> t.hours >> t.minutes >> t.seconds;
return is;
}
};
int main() {
Time t1(9, 5, 30);
std::cout << t1 << std::endl; // 09:05:30
Time t2;
std::cout << "Enter H M S: ";
std::cin >> t2; // User enters: 14 30 0
std::cout << t2 << std::endl; // 14:30:00
}
Problem: Create a Config class that outputs in JSON-like format.
Requirements:
- Store app name, version, and debug flag
- Output as formatted JSON with indentation
- Handle boolean as "true"/"false"
Show Solution
#include <iostream>
#include <string>
class Config {
public:
std::string appName;
std::string version;
bool debug;
Config(const std::string& name, const std::string& ver, bool dbg)
: appName(name), version(ver), debug(dbg) {}
friend std::ostream& operator<<(std::ostream& os, const Config& c) {
os << "{\n"
<< " \"appName\": \"" << c.appName << "\",\n"
<< " \"version\": \"" << c.version << "\",\n"
<< " \"debug\": " << (c.debug ? "true" : "false") << "\n"
<< "}";
return os;
}
};
int main() {
Config cfg("MyApp", "2.1.0", true);
std::cout << cfg << std::endl;
// Output:
// {
// "appName": "MyApp",
// "version": "2.1.0",
// "debug": true
// }
}
Problem: Implement a Currency class with proper monetary formatting.
Requirements:
- Store amount as double and currency code
- Output format: "$1,234.56" or "EUR 1,234.56"
- Use fixed precision of 2 decimals
Show Solution
#include <iostream>
#include <iomanip>
#include <string>
class Currency {
public:
double amount;
std::string code;
Currency(double amt, const std::string& c)
: amount(amt), code(c) {}
friend std::ostream& operator<<(std::ostream& os, const Currency& c) {
if (c.code == "USD") {
os << "$";
} else {
os << c.code << " ";
}
os << std::fixed << std::setprecision(2) << c.amount;
return os;
}
};
int main() {
Currency usd(1234.56, "USD");
Currency eur(999.99, "EUR");
Currency gbp(750.00, "GBP");
std::cout << usd << std::endl; // $1234.56
std::cout << eur << std::endl; // EUR 999.99
std::cout << gbp << std::endl; // GBP 750.00
}
Advanced Operators
Beyond arithmetic and comparison, C++ allows overloading subscript [], function call (), type conversion, and assignment operators for powerful, expressive class interfaces.
Subscript Operator: []
The subscript operator provides array-like access to your objects. You typically need two versions:
a const version for reading and a non-const version for writing. This enables your class to work
with both const and non-const contexts.
#include <iostream>
#include <stdexcept>
class SafeArray {
public:
static const int SIZE = 5;
SafeArray() {
for (int i = 0; i < SIZE; ++i) {
data[i] = 0;
}
}
static const for SIZE means it's shared across all
instances and available at compile-time. This foundation provides a safer alternative to raw
C-style arrays by adding range validation.
// Non-const version: allows modification
int& operator[](int index) {
if (index < 0 || index >= SIZE) {
throw std::out_of_range("Index out of bounds");
}
return data[index];
}
operator[] returns int& (a reference), which allows the
caller to both read and modify the element. Before returning, it validates that the index is within
bounds [0, SIZE-1]. If the index is invalid, it throws std::out_of_range exception.
This version is called when the SafeArray object itself is non-const.
// Const version: read-only access
const int& operator[](int index) const {
if (index < 0 || index >= SIZE) {
throw std::out_of_range("Index out of bounds");
}
return data[index];
}
private:
int data[SIZE];
};
operator[] returns const int&, preventing modification of the
element. The const at the end of the function signature means this version is called
when the SafeArray object is const. Both versions perform identical bounds checking. The compiler
automatically chooses the correct version based on the constness of the object - this is called
"const overloading."
int main() {
SafeArray arr;
arr[0] = 10; // Uses non-const version
arr[1] = 20;
arr[2] = 30;
std::cout << arr[0] << std::endl; // 10
arr[0] = 10, the non-const operator[] is called, returning a reference
that can be assigned to. Reading arr[0] also uses the non-const version because
arr is not const. The syntax is identical to using built-in arrays, making SafeArray
intuitive and natural to use.
const SafeArray& constRef = arr;
std::cout << constRef[1] << std::endl; // Uses const version
arr, we force the use of the const version of
operator[]. The const version prevents us from modifying elements (you couldn't write
constRef[1] = 50 - it would cause a compile error). This demonstrates how the compiler
enforces const-correctness.
try {
std::cout << arr[10] << std::endl; // Throws exception
} catch (const std::out_of_range& e) {
std::cout << "Error: " << e.what() << std::endl;
}
return 0;
}
std::out_of_range. The try-catch block catches the exception and displays an error
message. This safety feature prevents undefined behavior from out-of-bounds access - a common
source of bugs and security vulnerabilities in C programs using raw arrays.
Function Call Operator: ()
The function call operator makes your object "callable" like a function. Objects with this operator are called functors (function objects). They're incredibly useful with STL algorithms and can maintain state between calls, unlike regular functions.
#include <iostream>
#include <vector>
#include <algorithm>
// Functor that multiplies by a factor
class Multiplier {
public:
Multiplier(int factor) : factor_(factor) {}
int operator()(int value) const {
return value * factor_;
}
private:
int factor_;
};
operator() to multiply any value by that factor. The syntax
triple(5) calls operator(), making the object behave like a function. The stored
factor_ is state that persists between calls - something regular functions can't do
without global variables.
// Functor that counts calls
class Counter {
public:
Counter() : count_(0) {}
void operator()(int) {
++count_;
}
int getCount() const { return count_; }
private:
int count_;
};
operator() is called,
it increments an internal counter. The parameter is unused (hence the anonymous int),
but it's required to match the signature expected by STL algorithms like for_each.
The getCount() method retrieves the accumulated count.
int main() {
Multiplier triple(3);
std::cout << triple(5) << std::endl; // 15
std::cout << triple(10) << std::endl; // 30
triple(5)
multiplies 5 by 3, returning 15. The syntax is identical to calling a function, but we're actually
calling the operator() method on the object. This makes functors feel natural and intuitive.
std::vector<int> nums = {1, 2, 3, 4, 5};
// Use functor with transform
std::transform(nums.begin(), nums.end(), nums.begin(), Multiplier(2));
// nums is now {2, 4, 6, 8, 10}
for (int n : nums) std::cout << n << " ";
std::cout << std::endl;
std::transform applies the Multiplier(2) functor to each
element in the vector, doubling all values. We create a temporary Multiplier object with factor 2,
and transform calls its operator() for each element. The compiler can often inline these calls,
making functors very efficient.
// Count elements with for_each
Counter counter;
counter = std::for_each(nums.begin(), nums.end(), counter);
std::cout << "Count: " << counter.getCount() << std::endl; // 5
return 0;
}
for_each algorithm calls counter's operator() for each element. After processing
all 5 elements, count_ reaches 5. Importantly, for_each returns the
functor (by value), so we must capture the return value to get the updated count. This demonstrates
how functors can accumulate state across multiple operations - impossible with simple function pointers.
Type Conversion Operators
Type conversion operators allow your class to be implicitly or explicitly converted to other types.
Use explicit to prevent accidental implicit conversions, which can lead to subtle bugs.
#include <iostream>
#include <string>
class Percentage {
public:
Percentage(double value) : value_(value) {}
// Implicit conversion to double (the raw value)
operator double() const {
return value_;
}
operator double() conversion operator allows Percentage objects to be implicitly
converted to double. Notice there's no return type specified (the type is already in the name).
This implicit conversion means you can write double d = discount; without a cast.
The conversion returns the raw numeric value without the percentage formatting.
// Explicit conversion to string
explicit operator std::string() const {
return std::to_string(value_) + "%";
}
explicit keyword prevents automatic conversion to string. You must explicitly
request the conversion using static_cast<std::string>(discount). This is safer
than implicit conversion because the compiler won't surprise you by converting to string when you
didn't intend it. The conversion formats the value with a "%" suffix.
// Explicit conversion to bool (non-zero percentage)
explicit operator bool() const {
return value_ != 0.0;
}
private:
double value_;
};
explicit, which has a special meaning: it allows use in boolean
contexts (if statements, while loops, logical operators) but prevents accidental conversion to bool
in other contexts. For example, if (discount) works, but int x = discount;
would fail to compile (preventing implicit bool-to-int conversion).
int main() {
Percentage discount(25.5);
// Implicit conversion to double
double d = discount;
std::cout << "Double: " << d << std::endl; // 25.5
double d = discount; implicitly invokes operator double().
No cast is needed because the conversion is not marked explicit. This makes the code clean and
natural when you want to extract the numeric value.
// Can use in arithmetic (implicit double conversion)
double price = 100.0 * (1.0 - discount / 100.0);
std::cout << "Price after discount: $" << price << std::endl; // $74.5
discount appears in arithmetic expressions, it's automatically converted to double.
The expression discount / 100.0 converts discount to 25.5, then divides by 100.0 to get
0.255. This implicit conversion makes Percentage objects work naturally in mathematical calculations.
// Explicit conversion to string
std::string s = static_cast<std::string>(discount);
std::cout << "String: " << s << std::endl; // 25.500000%
explicit, we must use static_cast to
request it. Writing std::string s = discount; would be a compile error. The explicit
requirement forces you to be intentional about converting to string, preventing bugs where the
compiler might choose an unexpected conversion.
// Explicit conversion to bool in if statement
if (discount) {
std::cout << "Discount applied!" << std::endl;
}
return 0;
}
explicit, it works in the if statement because that's
a "boolean context" where the language expects a boolean value. The condition checks if the percentage
is non-zero. This is the safe way to provide bool conversion - it prevents unintended conversions
while allowing natural boolean testing.
explicit for conversions that might be
surprising or lossy. Only allow implicit conversion when it's obvious and safe. Many experienced
C++ developers mark all conversion operators and single-argument constructors as
explicit by default, removing the keyword only when implicit conversion is truly beneficial.
Copy and Move Assignment Operators
The assignment operator = is called when assigning to an existing object. For classes
managing resources (like dynamic memory), you need proper copy and move assignment to avoid memory
leaks and ensure correct behavior.
#include <iostream>
#include <cstring>
#include <utility>
class String {
public:
// Constructor
String(const char* str = "") {
size_ = std::strlen(str);
data_ = new char[size_ + 1];
std::strcpy(data_, str);
}
// Destructor
~String() {
delete[] data_;
}
new char[] and copies the input string. The destructor releases this memory with
delete[]. Because this class owns a resource, it needs custom copy/move operations
to ensure proper resource management - this is the "Rule of Five."
// Copy constructor
String(const String& other) {
size_ = other.size_;
data_ = new char[size_ + 1];
std::strcpy(data_, other.data_);
}
// Copy assignment operator
String& operator=(const String& other) {
if (this != &other) { // Self-assignment check
delete[] data_; // Free existing resource
size_ = other.size_;
data_ = new char[size_ + 1];
std::strcpy(data_, other.data_);
}
return *this;
}
this != &other check prevents self-assignment disasters - without it,
str = str; would delete the data before trying to copy it! The function deletes old
data, allocates new memory, and copies. Returning *this enables chaining:
a = b = c;.
// Move constructor
String(String&& other) noexcept
: data_(other.data_), size_(other.size_) {
other.data_ = nullptr;
other.size_ = 0;
}
&& rvalue
reference). Instead of allocating new memory and copying, it just takes ownership of other's data
pointer. The source is left with nullptr so its destructor won't delete the now-stolen
memory. noexcept tells STL containers this won't throw, enabling optimizations.
// Move assignment operator
String& operator=(String&& other) noexcept {
if (this != &other) {
delete[] data_;
data_ = other.data_;
size_ = other.size_;
other.data_ = nullptr;
other.size_ = 0;
}
return *this;
}
void print() const {
std::cout << (data_ ? data_ : "null") << std::endl;
}
private:
char* data_;
size_t size_;
};
data_ ? data_ : "null" checks if data_ is valid before dereferencing. After
an object is moved from, it's in a "valid but unspecified state" - safe to destroy or reassign,
but its contents are undefined.
int main() {
String s1("Hello");
String s2("World");
s2 = s1; // Copy assignment
s1.print(); // Hello
s2.print(); // Hello
s2 = s1; calls copy assignment because s1 is an lvalue (named object).
After the copy, both s1 and s2 contain "Hello", each with their own independent memory allocation.
Modifying one won't affect the other. This is safe but potentially expensive for large strings.
String s3("Temporary");
s3 = std::move(s1); // Move assignment
s3.print(); // Hello
s1.print(); // null (moved from)
return 0;
}
std::move(s1) casts s1 to an rvalue reference, triggering move assignment. After the
move, s3 owns the "Hello" string that s1 previously owned, and s1 is left with a null pointer.
The move is very efficient - just copying a pointer and an integer, no memory allocation or string
copying. Note: s1 is still a valid object (its destructor will safely run), but its content is gone.
- Rule of Three: If you define destructor, copy constructor, or copy assignment, define all three
- Rule of Five: Add move constructor and move assignment to the Rule of Three
- Rule of Zero: Best practice - use smart pointers (std::unique_ptr, std::shared_ptr) instead of raw pointers to avoid implementing these entirely!
Best Practices and Pitfalls
- Return
*thisfrom assignment operators - Make comparison operators
const - Implement
operator+usingoperator+= - Use
friendfor symmetric operators - Check for self-assignment in
operator= - Mark move operations
noexcept
- Don't overload operators with surprising behavior
- Don't overload
&&,||, or, - Don't return non-const reference from
operator[]on const object - Don't forget the dummy int in postfix ++/--
- Don't modify operands in binary operators
- Don't use implicit bool conversion carelessly
Practice Questions: Advanced Operators
Problem: Implement a simple string-to-string map using operator[].
Requirements:
- Use std::map internally
- operator[] should work for both get and set
- Return empty string for missing keys
Show Solution
#include <iostream>
#include <map>
#include <string>
class StringMap {
public:
std::string& operator[](const std::string& key) {
return data_[key]; // Creates entry if not exists
}
const std::string& operator[](const std::string& key) const {
static const std::string empty;
auto it = data_.find(key);
return (it != data_.end()) ? it->second : empty;
}
private:
std::map<std::string, std::string> data_;
};
int main() {
StringMap config;
config["name"] = "MyApp";
config["version"] = "1.0";
std::cout << config["name"] << std::endl; // MyApp
std::cout << config["missing"] << std::endl; // (empty)
}
Problem: Create a functor that checks if numbers are in a range.
Requirements:
- Store min and max values
- operator() returns true if value is in range [min, max]
- Test with std::count_if
Show Solution
#include <iostream>
#include <vector>
#include <algorithm>
class InRange {
public:
InRange(int min, int max) : min_(min), max_(max) {}
bool operator()(int value) const {
return value >= min_ && value <= max_;
}
private:
int min_, max_;
};
int main() {
std::vector<int> nums = {1, 5, 10, 15, 20, 25, 30};
InRange between10and20(10, 20);
int count = std::count_if(nums.begin(), nums.end(), between10and20);
std::cout << "Numbers in range [10,20]: " << count << std::endl; // 3
// Filter manually
for (int n : nums) {
if (between10and20(n)) {
std::cout << n << " ";
}
}
// Output: 10 15 20
}
Problem: Create a basic smart pointer class.
Requirements:
- Template class that wraps a raw pointer
- Implement operator* (dereference) and operator-> (member access)
- Delete the pointer in destructor
- Disable copy, allow move
Show Solution
#include <iostream>
#include <utility>
template<typename T>
class SmartPtr {
public:
explicit SmartPtr(T* ptr = nullptr) : ptr_(ptr) {}
~SmartPtr() { delete ptr_; }
// Disable copy
SmartPtr(const SmartPtr&) = delete;
SmartPtr& operator=(const SmartPtr&) = delete;
// Enable move
SmartPtr(SmartPtr&& other) noexcept : ptr_(other.ptr_) {
other.ptr_ = nullptr;
}
SmartPtr& operator=(SmartPtr&& other) noexcept {
if (this != &other) {
delete ptr_;
ptr_ = other.ptr_;
other.ptr_ = nullptr;
}
return *this;
}
T& operator*() const { return *ptr_; }
T* operator->() const { return ptr_; }
explicit operator bool() const { return ptr_ != nullptr; }
private:
T* ptr_;
};
struct Point {
int x, y;
void print() { std::cout << "(" << x << "," << y << ")" << std::endl; }
};
int main() {
SmartPtr<Point> p(new Point{3, 4});
(*p).print(); // (3,4) - using operator*
p->print(); // (3,4) - using operator->
if (p) {
std::cout << "Pointer is valid" << std::endl;
}
}
Problem: Create a length class with type conversion operators.
Requirements:
- Store length in meters
- Conversion to double returns meters
- Add method toCentimeters() and toFeet()
- Support arithmetic operators
Show Solution
#include <iostream>
class Meter {
public:
explicit Meter(double m = 0) : meters_(m) {}
// Conversion to double (meters)
explicit operator double() const { return meters_; }
double toCentimeters() const { return meters_ * 100.0; }
double toFeet() const { return meters_ * 3.28084; }
Meter operator+(const Meter& other) const {
return Meter(meters_ + other.meters_);
}
Meter operator-(const Meter& other) const {
return Meter(meters_ - other.meters_);
}
Meter& operator+=(const Meter& other) {
meters_ += other.meters_;
return *this;
}
private:
double meters_;
};
int main() {
Meter length(1.5);
std::cout << static_cast<double>(length) << " m" << std::endl; // 1.5 m
std::cout << length.toCentimeters() << " cm" << std::endl; // 150 cm
std::cout << length.toFeet() << " ft" << std::endl; // 4.92126 ft
Meter total = Meter(2.0) + Meter(3.5);
std::cout << static_cast<double>(total) << " m" << std::endl; // 5.5 m
}
Key Takeaways
Arithmetic Operators
Return new objects from binary operators. Implement + using +=. Use member functions or friends as appropriate.
Comparison Operators
Implement operator< and derive others from it. Use C++20 spaceship operator for cleaner code.
Stream Operators
Always return the stream reference. Use friend functions for << and >>. Make output const-correct.
Subscript Operator
Provide both const and non-const versions. Add bounds checking for safety. Return references for efficiency.
Functors
Objects with operator() can store state. They work great with STL algorithms. Mark noexcept when appropriate.
Best Practices
Follow the principle of least surprise. Check self-assignment. Use explicit for dangerous conversions.
Knowledge Check
Quick Quiz
Test your understanding of C++ operator overloading