Object-Oriented Programming

Operator Overloading

Overload C++ operators for custom types — arithmetic, comparison, stream insertion/extraction, subscript, function call, and conversion operators.

Operator Overloading

C++ lets you redefine the behaviour of built-in operators (+, -, *, ==, <<, [], etc.) for your own types. Done well, overloading makes custom types feel as natural as built-in types. Done poorly, it creates confusing code. The guiding principle: only overload when the meaning is obvious and matches the conventional use of the operator.


Basic Arithmetic Operators

cpp
#include <iostream>
using namespace std;

class Vector2D {
public:
    double x, y;

    Vector2D(double x = 0, double y = 0) : x(x), y(y) {}

    // Member operator: left operand is *this
    Vector2D operator+(const Vector2D& rhs) const {
        return {x + rhs.x, y + rhs.y};
    }

    Vector2D operator-(const Vector2D& rhs) const {
        return {x - rhs.x, y - rhs.y};
    }

    Vector2D operator*(double scalar) const {
        return {x * scalar, y * scalar};
    }

    Vector2D& operator+=(const Vector2D& rhs) {
        x += rhs.x; y += rhs.y;
        return *this;
    }

    double magnitude() const {
        return sqrt(x * x + y * y);
    }
};

// Non-member: allows scalar * vector (not just vector * scalar)
Vector2D operator*(double scalar, const Vector2D& v) {
    return {v.x * scalar, v.y * scalar};
}

int main() {
    Vector2D a(1.0, 2.0), b(3.0, 4.0);

    Vector2D sum  = a + b;
    Vector2D diff = b - a;
    Vector2D scaled = a * 3.0;
    Vector2D scaled2 = 3.0 * a;   // Uses non-member overload

    cout << "Sum: (" << sum.x << ", " << sum.y << ")\n";     // (4, 6)
    cout << "Scaled: (" << scaled.x << ", " << scaled.y << ")\n";  // (3, 6)
    cout << "|b| = " << b.magnitude() << "\n";  // 5

    return 0;
}

Comparison Operators

cpp
#include <iostream>
using namespace std;

class Fraction {
public:
    int num, den;

    Fraction(int n, int d) : num(n), den(d) {
        if (den < 0) { num = -num; den = -den; }   // Normalize sign
    }

    bool operator==(const Fraction& rhs) const {
        return num * rhs.den == rhs.num * den;
    }

    bool operator!=(const Fraction& rhs) const {
        return !(*this == rhs);
    }

    bool operator<(const Fraction& rhs) const {
        return num * rhs.den < rhs.num * den;
    }

    bool operator>(const Fraction& rhs) const {
        return rhs < *this;
    }

    bool operator<=(const Fraction& rhs) const {
        return !(rhs < *this);
    }

    bool operator>=(const Fraction& rhs) const {
        return !(*this < rhs);
    }
};

int main() {
    Fraction a(1, 2), b(2, 4), c(3, 4);

    cout << (a == b) << "\n";   // 1 (true — 1/2 == 2/4)
    cout << (a <  c) << "\n";   // 1 (true — 1/2 < 3/4)
    cout << (c >  a) << "\n";   // 1 (true)

    return 0;
}

Note: In C++20, defining operator<=> (the spaceship operator) automatically generates all six comparison operators.


Stream Operators (<< and >>)

These must be non-member functions (the left operand is ostream, not your class):

cpp
#include <iostream>
#include <sstream>
using namespace std;

class Complex {
public:
    double real, imag;
    Complex(double r = 0, double i = 0) : real(r), imag(i) {}
};

// Non-member — left operand is ostream
ostream& operator<<(ostream& os, const Complex& c) {
    os << c.real;
    if (c.imag >= 0) os << " + " << c.imag << "i";
    else             os << " - " << (-c.imag) << "i";
    return os;   // Return os to allow chaining: cout << a << b
}

// Non-member — reads into object
istream& operator>>(istream& is, Complex& c) {
    is >> c.real >> c.imag;
    return is;
}

int main() {
    Complex c1(3.0, 4.0), c2(1.0, -2.0);

    cout << c1 << "\n";    // 3 + 4i
    cout << c2 << "\n";    // 1 - 2i

    // Works with stringstream too
    ostringstream oss;
    oss << c1;
    cout << oss.str() << "\n";   // 3 + 4i

    Complex c3;
    istringstream iss("5.0 -3.0");
    iss >> c3;
    cout << c3 << "\n";    // 5 - 3i

    return 0;
}

Subscript Operator ([])

cpp
#include <iostream>
#include <stdexcept>
using namespace std;

class SafeArray {
    int    data[100];
    size_t size_;

public:
    SafeArray(size_t size) : size_(size) {
        for (size_t i = 0; i < size_; i++) data[i] = 0;
    }

    // Non-const version — allows modification
    int& operator[](size_t index) {
        if (index >= size_) throw out_of_range("Index out of bounds");
        return data[index];
    }

    // Const version — called on const objects
    const int& operator[](size_t index) const {
        if (index >= size_) throw out_of_range("Index out of bounds");
        return data[index];
    }

    size_t size() const { return size_; }
};

int main() {
    SafeArray arr(5);
    arr[0] = 10;
    arr[1] = 20;
    arr[2] = 30;

    for (size_t i = 0; i < arr.size(); i++) {
        cout << arr[i] << " ";
    }
    cout << "\n";   // 10 20 30 0 0

    try {
        arr[10] = 99;   // Throws!
    } catch (const out_of_range& e) {
        cout << "Error: " << e.what() << "\n";
    }

    return 0;
}

Function Call Operator (())

A class with operator() is called a functor — an object that behaves like a function:

cpp
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;

// Functor that multiplies by a factor stored as state
class Multiplier {
    double factor;
public:
    Multiplier(double f) : factor(f) {}

    double operator()(double x) const {
        return x * factor;
    }
};

// Functor for range-based filtering
class InRange {
    int min_, max_;
public:
    InRange(int min, int max) : min_(min), max_(max) {}
    bool operator()(int x) const { return x >= min_ && x <= max_; }
};

int main() {
    Multiplier double_it(2.0);
    Multiplier triple(3.0);

    cout << double_it(5.0) << "\n";  // 10
    cout << triple(5.0)    << "\n";  // 15

    vector<int> v = {1, 5, 3, 8, 2, 7, 4, 9, 6};

    // Count numbers in range 3-7
    int count = count_if(v.begin(), v.end(), InRange(3, 7));
    cout << "In [3,7]: " << count << "\n";  // 5

    // Transform with functor
    vector<double> nums = {1.0, 2.0, 3.0, 4.0};
    transform(nums.begin(), nums.end(), nums.begin(), double_it);
    for (double n : nums) cout << n << " ";
    cout << "\n";  // 2 4 6 8

    return 0;
}

Assignment Operator (=)

cpp
#include <iostream>
#include <cstring>
using namespace std;

class String {
    char* data;
    size_t len;

public:
    String(const char* s = "") : len(strlen(s)), data(new char[len + 1]) {
        strcpy(data, s);
    }

    // Copy assignment — copy-and-swap idiom
    String& operator=(String other) {     // Note: pass by VALUE (copies)
        swap(data, other.data);           // Swap with the copy
        swap(len,  other.len);
        return *this;                     // other (old data) destroyed on exit
    }

    ~String() { delete[] data; }

    const char* c_str() const { return data; }
};

int main() {
    String a("hello");
    String b = a;          // Copy constructor
    String c("world");
    c = a;                 // Copy assignment (operator=)

    cout << b.c_str() << "\n";   // hello
    cout << c.c_str() << "\n";   // hello

    return 0;
}

Conversion Operators

cpp
#include <iostream>
using namespace std;

class Celsius {
    double temp;
public:
    Celsius(double t) : temp(t) {}

    // Implicit conversion to double (use with care)
    operator double() const { return temp; }

    // Explicit conversion to bool (safe — only called with explicit cast)
    explicit operator bool() const { return temp > 0; }
};

int main() {
    Celsius c(25.0);

    double t = c;                  // Implicit conversion
    cout << t << "\n";             // 25

    if (static_cast<bool>(c)) {    // Explicit conversion
        cout << "Positive temp\n";
    }

    return 0;
}

Practical Example: Matrix Class

cpp
#include <iostream>
#include <vector>
#include <iomanip>
using namespace std;

class Matrix {
    int    rows, cols;
    vector<vector<double>> data;

public:
    Matrix(int r, int c, double init = 0.0)
        : rows(r), cols(c), data(r, vector<double>(c, init)) {}

    double& operator()(int r, int c)             { return data[r][c]; }
    const double& operator()(int r, int c) const { return data[r][c]; }

    Matrix operator+(const Matrix& rhs) const {
        Matrix result(rows, cols);
        for (int r = 0; r < rows; r++)
            for (int c = 0; c < cols; c++)
                result(r, c) = data[r][c] + rhs.data[r][c];
        return result;
    }

    Matrix operator*(const Matrix& rhs) const {
        Matrix result(rows, rhs.cols);
        for (int r = 0; r < rows; r++)
            for (int c = 0; c < rhs.cols; c++)
                for (int k = 0; k < cols; k++)
                    result(r, c) += data[r][k] * rhs.data[k][c];
        return result;
    }

    bool operator==(const Matrix& rhs) const {
        return data == rhs.data;
    }

    friend ostream& operator<<(ostream& os, const Matrix& m) {
        for (int r = 0; r < m.rows; r++) {
            os << "[ ";
            for (int c = 0; c < m.cols; c++) {
                os << setw(6) << fixed << setprecision(1) << m.data[r][c];
            }
            os << " ]\n";
        }
        return os;
    }
};

int main() {
    Matrix a(2, 2), b(2, 2);
    a(0,0) = 1; a(0,1) = 2;
    a(1,0) = 3; a(1,1) = 4;

    b(0,0) = 5; b(0,1) = 6;
    b(1,0) = 7; b(1,1) = 8;

    cout << "A:\n" << a;
    cout << "B:\n" << b;
    cout << "A + B:\n" << (a + b);
    cout << "A * B:\n" << (a * b);
    // A * B:
    // [ 19.0  22.0 ]
    // [ 43.0  50.0 ]

    return 0;
}

Summary

  • Overload operators to make custom types feel as natural as built-in types — follow conventions (e.g., + should be symmetric and non-mutating)
  • Binary arithmetic operators (+, -, *) are best implemented as non-member functions or const member functions
  • Compound assignment operators (+=, -=) must be member functions — they modify *this
  • Stream operators (<<, >>) must be non-member functions (first operand is ostream/istream)
  • operator[] should provide both const and non-const overloads
  • Functors (classes with operator()) are stateful — they outperform stateless functions when the operation needs captured state
  • The copy-and-swap idiom in operator= is exception-safe and handles self-assignment automatically
  • Never overload &&, ||, or , — they lose their short-circuit/sequencing properties when overloaded

Next, we'll dive into memory management — pointers, references, dynamic allocation, and the tools C++ gives you to do it safely.