C++ Operator Overloading Complete Guide - Learn to overload operators in C++
|

C++ Operator Overloading: Complete Guide with Examples

Back to C++ RoadmapC++ Programming Course • 65 Lessons

What Is Operator Overloading?

C++ lets you redefine how operators behave for your custom types. When you write a + b where a and b are integers, the compiler knows what to do. But what if they are Vector2D objects? Operator overloading lets you teach the compiler how to add vectors, compare dates, or print your objects with cout.

This is one of the features that makes C++ uniquely expressive. You can write vec1 + vec2 instead of vec1.add(vec2), making code read almost like mathematics. But with great power comes responsibility — overloading should make your code more intuitive, not less. If someone reads a + b, they should immediately understand what it does.

Under the hood, a + b is just syntactic sugar for a function call. The compiler translates it to either a.operator+(b) (member function) or operator+(a, b) (free function). This means overloading an operator is simply defining a function with a special name.

Basic Syntax

Here is the simplest example — a Point struct with an overloaded + operator:

#include <iostream>
using namespace std;

struct Point {
    double x, y;

    // Overload + as a member function
    Point operator+(const Point& other) const {
        return {x + other.x, y + other.y};
    }
};

int main() {
    Point a{1.0, 2.0};
    Point b{3.0, 4.0};
    Point c = a + b;  // calls a.operator+(b)

    cout << "(" << c.x << ", " << c.y << ")" << endl;
    // Output: (4, 6)
    return 0;
}

The function name is operator+. It takes the right-hand operand as a parameter (the left-hand operand is *this). The const at the end means calling + does not modify the left operand — it returns a new Point.

Arithmetic Operators (+, -, *, /)

Arithmetic operators should return a new object (by value), not modify the operands. Here is a complete Vector2D class with all four basic arithmetic operators:

#include <iostream>
#include <cmath>
using namespace std;

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

    // Arithmetic operators
    Vector2D operator+(const Vector2D& v) const { return {x + v.x, y + v.y}; }
    Vector2D operator-(const Vector2D& v) const { return {x - v.x, y - v.y}; }

    // Scalar multiplication
    Vector2D operator*(double scalar) const { return {x * scalar, y * scalar}; }
    Vector2D operator/(double scalar) const {
        if (scalar == 0) throw runtime_error("Division by zero");
        return {x / scalar, y / scalar};
    }

    // Compound assignment — modifies *this and returns reference
    Vector2D& operator+=(const Vector2D& v) {
        x += v.x;
        y += v.y;
        return *this;
    }

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

    // Friend for stream output
    friend ostream& operator<<(ostream& os, const Vector2D& v);
};

ostream& operator<<(ostream& os, const Vector2D& v) {
    return os << "(" << v.x << ", " << v.y << ")";
}

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

    cout << "a + b = " << a + b << endl;       // (4, 6)
    cout << "a - b = " << a - b << endl;       // (2, 2)
    cout << "a * 2 = " << a * 2 << endl;       // (6, 8)
    cout << "b / 2 = " << b / 2 << endl;       // (0.5, 1)

    a += b;
    cout << "a += b: " << a << endl;            // (4, 6)

    return 0;
}

Notice the difference between operator+ (returns a new object by value) and operator+= (modifies *this and returns a reference). A common pattern is to implement + in terms of +=:

Vector2D operator+(const Vector2D& v) const {
    Vector2D result = *this;  // copy
    result += v;              // reuse +=
    return result;
}

This avoids code duplication and ensures + and += always agree.

Comparison Operators (==, !=, <, >)

Comparison operators return bool. If you define ==, you should also define !=. If you define <, consider defining all six relational operators — or use C++20’s spaceship operator (covered later).

#include <iostream>
#include <string>
using namespace std;

class Date {
    int year, month, day;
public:
    Date(int y, int m, int d) : year(y), month(m), day(d) {}

    bool operator==(const Date& d) const {
        return year == d.year && month == d.month && day == d.day;
    }

    bool operator!=(const Date& d) const {
        return !(*this == d);  // reuse ==
    }

    bool operator<(const Date& d) const {
        if (year != d.year) return year < d.year;
        if (month != d.month) return month < d.month;
        return day < d.day;
    }

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

    friend ostream& operator<<(ostream& os, const Date& d) {
        return os << d.year << "-"
                  << (d.month < 10 ? "0" : "") << d.month << "-"
                  << (d.day < 10 ? "0" : "") << d.day;
    }
};

int main() {
    Date today(2026, 6, 12);
    Date launch(2026, 1, 15);

    cout << boolalpha;
    cout << today << " == " << launch << "? " << (today == launch) << endl;
    cout << today << " > " << launch << "? " << (today > launch) << endl;
    // 2026-06-12 == 2026-01-15? false
    // 2026-06-12 > 2026-01-15? true
    return 0;
}

The pattern here is important: implement == and < from scratch, then build every other comparison on top of them. This eliminates bugs from inconsistent logic.

Stream Operators (<< and >>)

The stream insertion operator << lets you print your objects with cout. It must be a free function (not a member) because the left operand is ostream, not your class. You typically declare it as a friend so it can access private data:

#include <iostream>
#include <string>
using namespace std;

class Color {
    int r, g, b;
public:
    Color(int r, int g, int b) : r(r), g(g), b(b) {}

    // Output: Color(255, 128, 0)
    friend ostream& operator<<(ostream& os, const Color& c) {
        return os << "Color(" << c.r << ", " << c.g << ", " << c.b << ")";
    }

    // Input: reads three ints
    friend istream& operator>>(istream& is, Color& c) {
        is >> c.r >> c.g >> c.b;
        // Validate
        if (c.r < 0 || c.r > 255 || c.g < 0 || c.g > 255 || c.b < 0 || c.b > 255) {
            is.setstate(ios::failbit);
        }
        return is;
    }
};

int main() {
    Color orange(255, 165, 0);
    cout << orange << endl;  // Color(255, 165, 0)

    Color input(0, 0, 0);
    cout << "Enter R G B: ";
    cin >> input;
    if (cin) {
        cout << "You entered: " << input << endl;
    } else {
        cout << "Invalid color values!" << endl;
    }
    return 0;
}

Key rules: operator<< takes ostream& and returns ostream& so you can chain calls (cout << a << b). The second parameter is const because printing should not modify the object. operator>> takes istream& and a non-const reference (it needs to write to the object).

Subscript Operator []

The subscript operator lets your class behave like an array or map. It must be a member function. You typically provide two overloads — one const (for read access) and one non-const (for write access):

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

class IntArray {
    int* data;
    int size_;
public:
    IntArray(int n) : data(new int[n]()), size_(n) {}
    ~IntArray() { delete[] data; }

    int size() const { return size_; }

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

    // Const: read-only access
    const int& operator[](int index) const {
        if (index < 0 || index >= size_)
            throw out_of_range("Index out of bounds");
        return data[index];
    }
};

int main() {
    IntArray arr(5);

    arr[0] = 10;    // calls non-const version
    arr[1] = 20;
    arr[2] = 30;

    const IntArray& ref = arr;
    cout << ref[0] << endl;   // calls const version: 10
    // ref[0] = 99;           // ERROR: const version returns const ref

    return 0;
}

The bounds checking in operator[] is optional but recommended. The standard library’s std::vector does not bounds-check [] (for performance) but does check .at().

Assignment Operator =

The copy assignment operator is called when you assign one existing object to another. It is closely related to the copy constructor but has an extra step: cleaning up the old state before copying new data.

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

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

    // Copy constructor
    DynString(const DynString& other) {
        len = other.len;
        data = new char[len + 1];
        strcpy(data, other.data);
    }

    // Copy assignment (copy-and-swap idiom)
    DynString& operator=(DynString other) {  // note: pass by value
        swap(data, other.data);
        swap(len, other.len);
        return *this;
        // other is destroyed here, freeing old data
    }

    ~DynString() { delete[] data; }

    friend ostream& operator<<(ostream& os, const DynString& s) {
        return os << s.data;
    }
};

int main() {
    DynString a("Hello");
    DynString b("World");

    cout << "a = " << a << endl;  // Hello
    b = a;                         // copy assignment
    cout << "b = " << b << endl;  // Hello

    return 0;
}

The copy-and-swap idiom is the safest way to implement operator=. By taking the parameter by value, the copy constructor handles allocation. Then we swap the internals. The old data is freed when the parameter goes out of scope. This is automatically exception-safe and handles self-assignment correctly.

Increment and Decrement (++, –)

C++ distinguishes prefix (++x) from postfix (x++) using a dummy int parameter:

#include <iostream>
using namespace std;

class Counter {
    int value;
public:
    Counter(int v = 0) : value(v) {}

    // Prefix: ++counter (increment, return new value)
    Counter& operator++() {
        ++value;
        return *this;
    }

    // Postfix: counter++ (return old value, then increment)
    Counter operator++(int) {  // dummy int distinguishes postfix
        Counter old = *this;
        ++value;
        return old;
    }

    // Same pattern for --
    Counter& operator--() { --value; return *this; }
    Counter operator--(int) { Counter old = *this; --value; return old; }

    friend ostream& operator<<(ostream& os, const Counter& c) {
        return os << c.value;
    }
};

int main() {
    Counter c(5);

    cout << "c   = " << c << endl;    // 5
    cout << "++c = " << ++c << endl;  // 6
    cout << "c++ = " << c++ << endl;  // 6 (returns old value)
    cout << "c   = " << c << endl;    // 7

    return 0;
}

Prefix returns a reference (the modified object). Postfix returns a copy (the old value). This is why ++i is slightly more efficient than i++ — postfix creates a temporary copy.

Member vs Free Functions

Some operators must be members, some must be free, and some can be either. Here is the decision guide:

Must be member functions:

  • = (assignment)
  • [] (subscript)
  • -> (member access)
  • () (function call)

Must be free functions:

  • << and >> (stream operators) — the left operand is a stream, not your class

Prefer free functions (often as friends):

  • Symmetric binary operators: +, -, *, ==, !=, <
  • Reason: if + is a member of Vector2D, then vec * 2.0 works but 2.0 * vec does not (the left operand must be a Vector2D). A free function handles both:
// Free function: works for both vec * 2.0 and 2.0 * vec
Vector2D operator*(const Vector2D& v, double s) { return {v.x * s, v.y * s}; }
Vector2D operator*(double s, const Vector2D& v) { return v * s; }  // reuse

Prefer member functions:

  • Unary operators: ++, --, - (negation), !
  • Compound assignment: +=, -=, *=

The Spaceship Operator <=> (C++20)

Before C++20, implementing all six comparison operators meant writing a lot of boilerplate (as we saw with the Date class). C++20 introduces the three-way comparison operator <=> (informally called the spaceship operator). Define it once, and the compiler generates all six comparisons:

#include <iostream>
#include <compare>
using namespace std;

class Version {
    int major, minor, patch;
public:
    Version(int ma, int mi, int p) : major(ma), minor(mi), patch(p) {}

    // One operator replaces all six comparisons
    auto operator<=>(const Version&) const = default;

    friend ostream& operator<<(ostream& os, const Version& v) {
        return os << v.major << "." << v.minor << "." << v.patch;
    }
};

int main() {
    Version v1(2, 1, 0), v2(2, 3, 1);

    cout << boolalpha;
    cout << v1 << " < " << v2 << "? " << (v1 < v2) << endl;    // true
    cout << v1 << " == " << v2 << "? " << (v1 == v2) << endl;  // false
    cout << v1 << " >= " << v2 << "? " << (v1 >= v2) << endl;  // false
    return 0;
}

The = default tells the compiler to compare members in declaration order (major first, then minor, then patch) — exactly what we want for version numbers. For custom logic, implement the body yourself and return std::strong_ordering, std::weak_ordering, or std::partial_ordering.

Rules and Best Practices

Only overload when the meaning is intuitive. Adding two vectors with + makes sense. Using + to delete something from a container does not. If a reader cannot guess what an operator does without looking at the implementation, use a named function instead.

Overload related operators together. If you define ==, define !=. If you define +, define +=. Inconsistent operator sets are a common source of bugs.

Preserve the semantics of built-in operators. + should not modify its operands. = should return *this by reference. ++ prefix should return a reference, postfix a copy. Following these conventions means your types work correctly with generic code.

You cannot overload these operators: :: (scope resolution), . (member access), .* (member pointer access), ?: (ternary), sizeof, typeid, and the cast operators (static_cast, etc.).

You cannot create new operators. You can only overload existing C++ operators. There is no operator** for exponentiation.

You cannot change precedence or associativity. * always binds tighter than +, regardless of what you overload them to do.

Real-World Example: Matrix Class

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

class Matrix {
    vector<vector<double>> data;
    int rows_, cols_;

public:
    Matrix(int r, int c, double val = 0)
        : data(r, vector<double>(c, val)), rows_(r), cols_(c) {}

    int rows() const { return rows_; }
    int cols() const { return cols_; }

    // Subscript: mat[i][j] — returns a row
    vector<double>& operator[](int i) { return data[i]; }
    const vector<double>& operator[](int i) const { return data[i]; }

    // Addition
    Matrix operator+(const Matrix& m) const {
        if (rows_ != m.rows_ || cols_ != m.cols_)
            throw invalid_argument("Matrix size mismatch");
        Matrix result(rows_, cols_);
        for (int i = 0; i < rows_; i++)
            for (int j = 0; j < cols_; j++)
                result[i][j] = data[i][j] + m[i][j];
        return result;
    }

    // Matrix multiplication
    Matrix operator*(const Matrix& m) const {
        if (cols_ != m.rows_)
            throw invalid_argument("Cannot multiply: column/row mismatch");
        Matrix result(rows_, m.cols_);
        for (int i = 0; i < rows_; i++)
            for (int j = 0; j < m.cols_; j++)
                for (int k = 0; k < cols_; k++)
                    result[i][j] += data[i][k] * m[k][j];
        return result;
    }

    // Scalar multiplication
    Matrix operator*(double s) const {
        Matrix result(rows_, cols_);
        for (int i = 0; i < rows_; i++)
            for (int j = 0; j < cols_; j++)
                result[i][j] = data[i][j] * s;
        return result;
    }

    // Equality
    bool operator==(const Matrix& m) const {
        if (rows_ != m.rows_ || cols_ != m.cols_) return false;
        for (int i = 0; i < rows_; i++)
            for (int j = 0; j < cols_; j++)
                if (data[i][j] != m[i][j]) return false;
        return true;
    }

    // Stream output
    friend ostream& operator<<(ostream& os, const Matrix& m) {
        for (int i = 0; i < m.rows_; i++) {
            os << "| ";
            for (int j = 0; j < m.cols_; j++)
                os << setw(6) << m[i][j] << " ";
            os << "|" << endl;
        }
        return os;
    }
};

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

    Matrix b(2, 2);
    b[0][0] = 5; b[0][1] = 6;
    b[1][0] = 7; b[1][1] = 8;

    cout << "A + B:" << endl << a + b << endl;
    cout << "A * B:" << endl << a * b << endl;
    cout << "A * 10:" << endl << a * 10 << endl;

    return 0;
}

This Matrix class demonstrates multiple overloaded operators working together. The [] operator returns a row vector, so mat[i][j] uses the built-in vector::operator[] for the second index. This is a common pattern for 2D containers.

Practice Exercises

Exercise 1: Create a Fraction class that overloads +, -, *, /, ==, <, and <<. Fractions should auto-simplify using GCD. Test with Fraction(1,2) + Fraction(1,3)5/6.

Exercise 2: Create a BigInt class that stores digits in a vector and overloads +, ==, <, and << to handle numbers larger than long long.

Exercise 3: Create a SmartArray class that overloads [] with bounds checking, + to concatenate arrays, and == to compare element-by-element.

Exercise 4: Add a <=> operator to the Date class from this lesson (requires C++20). Verify that all six comparison operators work with only that one definition.

Summary

Operator overloading lets your classes integrate seamlessly with C++ syntax. You learned how to overload arithmetic operators, comparisons, stream operators, subscript, assignment, and increment/decrement. The key takeaways: use the const qualifier correctly, return types matter (value vs reference), prefer free functions for symmetric operators, and never overload an operator if the meaning is not immediately obvious. The C++20 spaceship operator eliminates comparison boilerplate entirely. In the next lesson, you will learn about enumerations — a way to define named constants that make your code safer and more readable.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *