Module 7.5

C++ Operator Overloading

Make your custom classes work with standard operators like +, -, ==, and <<. Learn how to create intuitive interfaces that let your objects behave just like built-in types!

45 min read
Intermediate
Hands-on Examples
What You'll Learn
  • Arithmetic operators (+, -, *, /)
  • Comparison operators (==, <, >)
  • Stream operators (<<, >>)
  • Subscript operator []
  • Best practices and pitfalls
Contents
01

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.

C++ Feature

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;
    }
};
Each operator function returns a new Vector2D object rather than modifying the existing one. This follows the principle of least surprise - when you write 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;
}
Notice we need both 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;
    }
};
The prefix versions (++x, --x) return a reference to the modified object, enabling chaining like ++++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;
}
Implementing 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
}
02

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;
}
When implementing 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;
}
Notice how 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;
}
The = 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.
Comparison Categories: C++20 defines three ordering categories: 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
}
03

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;
}
The function returns 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) {}
The Point3D class represents a point in three-dimensional space with x, y, and z coordinates stored as doubles. The constructor provides default values of 0 for all coordinates, making it easy to create an "empty" point that can later be populated with input data. This class will demonstrate both input and output operators working together.
    // Output operator
    friend std::ostream& operator<<(std::ostream& os, const Point3D& p) {
        os << "(" << p.x << ", " << p.y << ", " << p.z << ")";
        return os;
    }
The output operator formats the point as "(x, y, z)" for display. Notice the Point3D parameter is 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;
    }
};
The input operator is critically different from output: the Point3D parameter is a non-const reference because we're modifying its values. The extraction operator >> 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
Here we create a default Point3D object (initially 0, 0, 0) and prompt the user for input. When the user types three space-separated numbers like "1.5 2.5 3.5" and presses Enter, the overloaded >> 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;
}
Finally, we display the point using the overloaded << 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.
Input Validation: Production code should check for input errors using 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>
The <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) {}
The Product class stores three pieces of data: a product name (string), price (double), and quantity (integer). This simple structure represents items in an inventory system. The constructor uses an initializer list to set all three fields. We'll use this class to demonstrate how formatting can align data in columns for easy reading.
    friend std::ostream& operator<<(std::ostream& os, const Product& p) {
        os << std::left << std::setw(20) << p.name
The first formatting line sets left alignment with 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
For the price, we switch to right alignment (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;
    }
};
The quantity uses 8 characters of width and inherits the right alignment from the previous setting. Adding " units" as literal text provides context. The function returns the stream reference (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;
}
The main function demonstrates the formatted output in action. After creating three products, it prints a header row with column names, followed by a separator line created using 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.
Output Preview:
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
}
04

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;
        }
    }
The SafeArray class wraps a fixed-size array with bounds checking. The constructor initializes all five elements to zero. Using 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];
    }
The non-const 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];
};
The const 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
When we write 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
By creating a const reference to 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;
}
Attempting to access index 10 (valid range is 0-4) triggers the bounds check, which throws 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_;
};
The Multiplier class demonstrates a simple functor. It stores a multiplication factor in its constructor and defines 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_;
};
The Counter functor demonstrates state management. Each time 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
Creating a Multiplier with factor 3, we can call it like a function: 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;
Here's where functors shine: 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;
}
The 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.
Functors vs Lambdas: Modern C++ (C++11+) introduced lambda expressions, which are essentially compiler-generated functors. Lambdas are more convenient for simple cases, but explicit functor classes are still valuable for complex logic, reusability, and when you need multiple operator() overloads.

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_;
    }
The 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_) + "%";
    }
The 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_;
};
The bool conversion is 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
The assignment 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
When 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%
Because the string conversion is 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;
}
Even though the bool conversion is 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.
Implicit vs Explicit: Use 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_;
    }
The String class manages dynamic memory with a raw pointer. The constructor allocates memory with 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_);
    }
The copy constructor creates a completely independent copy by allocating new memory and copying the string data. This is a "deep copy" - both objects own their own memory. Without this, the default copy constructor would just copy the pointer, leading to double-deletion when both destructors run (classic bug!).
    // 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;
    }
Copy assignment handles assigning to an existing object (unlike the constructor). The critical 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;
    }
The move constructor "steals" resources from a temporary object (indicated by && 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;
    }
Move assignment is like move construction but for existing objects. It deletes the current data (unlike move constructor, which starts with uninitialized memory), then steals the source's resources. The self-assignment check is technically unnecessary for moves (you rarely move from yourself), but it's defensive programming. This is dramatically more efficient than copy assignment for large strings.
    void print() const {
        std::cout << (data_ ? data_ : "null") << std::endl;
    }
    
private:
    char* data_;
    size_t size_;
};
The print method safely handles the null pointer case (which occurs after a move). The ternary operator 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
The statement 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.
Critical Rules:
  • 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

Do's
  • Return *this from assignment operators
  • Make comparison operators const
  • Implement operator+ using operator+=
  • Use friend for symmetric operators
  • Check for self-assignment in operator=
  • Mark move operations noexcept
Don'ts
  • 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

1 Which operator CANNOT be overloaded in C++?
2 Why must operator<< for output be a non-member (or friend) function?
3 What distinguishes postfix operator++ from prefix in function signature?
4 What is the C++20 "spaceship operator"?
5 Why should operator= check for self-assignment?
6 What is a functor (function object) in C++?
Answer all questions to check your score