Skip to content

Operator Overloading

Operator overloading allows you to define custom behaviors for C++ standard operators (such as +, -, *, <<, []) when applied to your user-defined classes. This allows your classes to act with the same intuitive syntax as built-in types (like int or float).


1. Member vs. Non-Member Overloads

When overloading an operator, you must choose between defining it as a member function of the class or as a non-member function (often declared as a friend to access private members).

1. Member Operator Overload

A member operator uses the implicit this pointer as the left-hand operand.

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

    // Binary + operator overloaded as member function
    Vector2D operator+(const Vector2D& other) const {
        return Vector2D(x + other.x, y + other.y);
    }
};

Vector2D v1{1.0, 2.0};
Vector2D v2{3.0, 4.0};
Vector2D v3 = v1 + v2; // Equivalent to: v1.operator+(v2)

2. Non-Member (Friend) Operator Overload

If the left-hand operand of an operator is a different type (like a float or an output stream), the operator cannot be a member function. It must be written as a non-member function.

For example, supporting scalar multiplication on both sides:

// Supports: Vector2D v2 = v1 * 2.0; (Member function works)
// But what about: Vector2D v2 = 2.0 * v1;? 
// The left operand is double, so double.operator*(Vector2D) is impossible.

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

    // Declare non-member function as friend to access private members
    friend Vector2D operator*(double scalar, const Vector2D& vec);
};

// Non-member definition
Vector2D operator*(double scalar, const Vector2D& vec) {
    return Vector2D(vec.x * scalar, vec.y * scalar);
}

2. Stream Insertion (<<) and Extraction (>>)

Interfacing with streams like std::cout or std::cin requires overloading the shift operators << and >>. These must be non-member functions because std::ostream / std::istream are on the left-hand side.

They must return a reference to the stream object (std::ostream&) to support method chaining (e.g. std::cout << a << b;).

#include <iostream>
#include <string>

class User {
    std::string name;
    int age;
public:
    User(const std::string& n, int a) : name(n), age(a) {}

    friend std::ostream& operator<<(std::ostream& os, const User& user);
    friend std::istream& operator>>(std::istream& is, User& user);
};

std::ostream& operator<<(std::ostream& os, const User& user) {
    os << "User: " << user.name << " (Age: " << user.age << ")";
    return os; // Returns stream reference for chaining
}

std::istream& operator>>(std::istream& is, User& user) {
    is >> user.name >> user.age;
    return is;
}

3. Increment and Decrement Overloads

To distinguish between the prefix (++x) and postfix (x++) forms, C++ uses a dummy int parameter in the signature for the postfix form.

class Counter {
    int count = 0;
public:
    // 1. Prefix: ++c (Returns reference to updated object)
    Counter& operator++() {
        count++;
        return *this;
    }

    // 2. Postfix: c++ (Returns copy of old object, takes dummy int parameter)
    Counter operator++(int) {
        Counter temp = *this; // Keep copy of old state
        count++;              // Perform increment
        return temp;          // Return old state
    }
};
Note: Postfix creation of a temp object is why prefix ++i is preferred for loops where the old value is not needed.

4. The Copy-and-Swap Idiom

When overloading the copy assignment operator (operator=), you must ensure exception safety and guard against self-assignment (e.g. x = x).

The Copy-and-Swap Idiom is the cleanest, most robust pattern to handle assignment safely.

  1. Leverage the copy constructor to create a temporary copy (handles allocation and exception checks first).
  2. Swap the contents of the temporary copy with the current object.
  3. Let the temporary object go out of scope, destroying the old resources automatically.
#include <algorithm>

class CustomArray {
    size_t size;
    int* data;
public:
    CustomArray(size_t s) : size(s), data(new int[s]) {}
    ~CustomArray() { delete[] data; }

    // Copy Constructor
    CustomArray(const CustomArray& other) : size(other.size), data(new int[other.size]) {
        std::copy(other.data, other.data + size, data);
    }

    // Friend swap function
    friend void swap(CustomArray& first, CustomArray& second) noexcept {
        using std::swap;
        swap(first.size, second.size);
        swap(first.data, second.data);
    }

    // Copy Assignment Operator using Copy-and-Swap
    // Note: passing by value triggers the copy constructor automatically!
    CustomArray& operator=(CustomArray other) {
        swap(*this, other); // Swap local copy contents with this
        return *this;
    } // 'other' goes out of scope here, automatically deleting old memory
};

5. Non-Overloadable Operators

C++ forbids overloading a handful of operators to maintain syntax integrity: - . (Member access) - .* (Member pointer dereference) - :: (Scope resolution) - ?: (Conditional / Ternary) - sizeof (Object size)