CODE: CPP Operator Overload

Operator overloading in C++ allows user-defined types to behave like built-in types by defining custom behavior for operators such as +, ==, or [].In Modern C++, it is tightly connected to const correctness, move semantics, and generic programming, forming the foundation for functors and lambdas.

While operator overloading has existed since early C++, Modern C++ (C++11–C++23) refined how we design, implement, and reason about it — especially with move semantics, constexpr, noexcept, and the spaceship operator.

Why Operator Overloading Exists

C++ is built on the philosophy:

“User-defined types should behave like built-in types.”

Built-in types:

cpp
int a = 5 + 3;
double x = y * z;

Custom types should feel the same:

cpp
Vector v3 = v1 + v2;
Matrix m = a * b;

Without overloading:

cpp
Vector v3 = v1.add(v2);

Overloading improves:

  • Expressiveness
  • Readability
  • Generic programming compatibility
  • STL integration

What Is Operator Overloading?

Operator overloading means defining custom behavior for operators when used with user-defined types.

Example:

cpp
class Vector {
public:
    int x, y;

    Vector(int x, int y) : x(x), y(y) {}

    Vector operator+(const Vector& other) const {
        return Vector(x + other.x, y + other.y);
    }
};

Usage:

cpp
Vector v1(1,2);
Vector v2(3,4);

Vector v3 = v1 + v2;

The last const means:

This function does NOT modify the current object (`*this`).

How the Compiler Sees It

When you write:

cpp
v1 + v2;

The compiler transforms it to:

cpp
v1.operator+(v2);

Operators are just special function calls.


Member vs Non-Member Overloading

Member version:

cpp
Vector operator+(const Vector& other) const;

Limitation:

  • Left operand must be Vector

Example:

cpp
class Rational {
	int n {0};
	int d {1};
public:
    Rational(int numerator = 0, int denominator =1): n(numerator), d(denominator) {}
    Rational(const Rational & rhs) : n(rhs.n), d(rhs.d) {} // copy constractor
    ~Rational();

	int numerator() const { return n; };
	int denominator() const { return d; };
	
	Rational& operator = (const Rational&) { // assignment
		if (this != &rhs) {
			n = rhs.numerator();
			d = rhs.denominator();
		}
		return *this;
	} 
	Rational operator + (const Rational&) const {
		return Rational((n * rhs.d) + (d * rhs.n), d * rhs.d);
	}
	Rational operator - (const Rational&) const {
		return Rational((n * rhs.d) - (d * rhs.n), d * rhs.d);
	}
	Rational operator * (const Rational&) const {
		return Rational((n * rhs.d) * (d * rhs.n), d * rhs.d);
	}
	Rational operator / (const Rational&) const {
		return Rational((n * rhs.d) + (d * rhs.n), d * rhs.d);
	}
	
};

Usage:

cpp
Vector2 a(1, 2);
Vector2 b(3, 4);

Vector2 c = a + b;

Why const matters

  • Ensures no mutation
  • Enables use with const objects
  • Signals intent clearly

Non-member version (recommended for symmetry):

cpp
Vector operator+(const Vector& lhs, const Vector& rhs) {
    return Vector(lhs.x + rhs.x, lhs.y + rhs.y);
}

This is more flexible and often preferred in Modern C++.


Operators That Can Be Overloaded

Arithmetic: + - * / %

Comparison: == != < > <= >=

Increment / decrement: ++ --

Assignment: = += -= *=

Subscript: []

Function call: ()

Stream: << >>

Operators That MUST Be Members

= [] () ->

Operators That Cannot Be Overloaded

. :: ?: sizeof typeid

**Operators can only be overloaded for user-defined types**

That means:

  • ✅ Classes
  • ✅ Structs
  • ❌ Built-in types (int, float, double, pointers)

This is intentionally restricted to protect the language from abuse.

❌ Not allowed

cpp
int operator+(int a, int b) {
    return a - b;
}

✅ Allowed

cpp
class Number {
public:
    int value;

    Number operator+(const Number& other) const {
        return Number{value + other.value};
    }
};


Examples:

Overloading `operator=`

cpp
class Buffer {
public:
    int* data;
    size_t size;

    Buffer(size_t s) : size(s) {
        data = new int[s];
    }

    ~Buffer() {
        delete[] data;
    }

    Buffer& operator=(const Buffer& other) {
        if (this != &other) {
            delete[] data;
            size = other.size;
            data = new int[size];
            std::copy(other.data, other.data + size, data);
        }
        return *this;
    }
};

Important:

  • Return reference
  • Handle self-assignment
  • Deep copy

This connects to:

Rule of Five (Modern C++)

If you define:

  • Destructor
  • Copy constructor
  • Copy assignment

You probably need:

  • Move constructor
  • Move assignment

Overloading Comparison Operators

Comparison operators allow objects to be compared logically.

Example: Equality

cpp
class User {
public:
    int id;

    bool operator==(const User& other) const {
        return id == other.id;
    }
};

Usage:

cpp
User a{1};
User b{1};

if (a == b) {
    // same user
}

Why this matters

  • Enables STL algorithms
  • Supports containers like std::set, std::map
  • Makes domain logic expressive

Comparison Operators (C++20)

C++20 introduced the spaceship operator:

cpp
auto operator<=>(const Vector&) const = default;

This generates:

md
==  !=  <  >  <=  >=

Clean and safe.


Overloading `operator[]`

Used in containers:

cpp
int& operator[](size_t index) {
    return data[index];
}

const int& operator[](size_t index) const {
    return data[index];
}

Two versions:

  • Mutable
  • Const

The Function Call Operator `operator()`

Now things get interesting.

cpp
class Multiplier {
    int factor;
public:
    Multiplier(int f) : factor(f) {}

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

Usage:

cpp
Multiplier times3(3);
int result = times3(10);  // 30

This object behaves like a function.

These are called Function Objects or Functors.


What Is a Functor?

A functor is:

An object that can be called like a function.

Implemented by overloading: operator()

Example in STL:

cpp
std::sort(vec.begin(), vec.end(), std::greater<>());

std::greater<> is a functor.

Why Functors Are Powerful

Compared to normal functions:

  • Can store state
  • Inlineable
  • Zero overhead
  • Template-friendly
  • Compile-time optimized

Example with state:

cpp
class Threshold {
    int limit;
public:
    Threshold(int l) : limit(l) {}

    bool operator()(int x) const {
        return x > limit;
    }
};


Transition: From Functors to Lambdas

Functors are powerful — but verbose.

Modern C++ introduced lambdas to replace most functors.

Instead of:

cpp
struct Add {
    int operator()(int a, int b) const {
        return a + b;
    }
};

We write:

cpp
auto add = [](int a, int b) {
    return a + b;
};

Lambdas are:

  • Anonymous functors
  • Inline
  • Concise
  • Capture variables
  • Foundation of modern STL usage