Operator Overloading

Learning Objectives

Overload arithmetic, comparison, stream and subscript operators with natural value semantics.

Keep equality, ordering and compound assignment consistent.

Know which operators can be overloaded in each language.

Avoid surprising side effects.

Prerequisites: Modules 12, 15.

Intuition and Definition

Operator overloading defines what built-in operators (+, ==, <<, []) mean for your types. It is syntactic sugar for function calls: a + b becomes a.operator+(b) or operator+(a, b). Use it only when the meaning is obvious — for value types such as vectors, money, matrices and dates.

Why It Matters

total = price * qty + shipping reads better than total = price.multiply(qty).add(shipping) — as long as the operators behave like the math everyone expects.

Syntax and Language Notes

Almost all (not ::, ., .*, ?:, sizeof); can't invent new ones or change precedence None (except + on String, built in)

Selected operators, must be public static; == and != must be overloaded together

Special methods: __add__, __eq__, __lt__, __getitem__, __iadd__, … None (methods like add() instead)

struct Vec2 {
    double x, y;
    Vec2 operator+(const Vec2& o) const { return {x + o.x, y + o.y}; }
    Vec2& operator+=(const Vec2& o) { x += o.x; y += o.y; return *this; }
    bool operator==(const Vec2&) const = default;      // C++20: compiler-generated
};

Complete Example

The same value type in Python with special methods:

#include <compare>
#include <iomanip>
#include <iostream>
#include <stdexcept>

class Money {
    long long cents_;
public:
    explicit Money(long long cents) : cents_(cents) {}
    static Money of(long long whole, int cents) { return Money(whole * 100 + cents); }

    Money& operator+=(const Money& o) { cents_ += o.cents_; return *this; }    // compound first
    friend Money operator+(Money a, const Money& b) { a += b; return a; }      // then binary in terms of it
    friend Money operator*(Money a, int n) { return Money(a.cents_ * n); }
    friend Money operator*(int n, Money a) { return a * n; }                    // keep it symmetric
    Money operator-() const { return Money(-cents_); }

    auto operator<=>(const Money&) const = default;                             // ==, !=, <, <=, >, >=

    friend std::ostream& operator<<(std::ostream& os, const Money& m) {
        long long c = m.cents_ < 0 ? -m.cents_ : m.cents_;
        return os << (m.cents_ < 0 ? "-$" : "$") << c / 100 << "." << std::setw(2) << std::setfill('0') << c % 100;
    }
};

class Readings {
    double values_[4]{11.9, 12.4, 12.6, 13.1};
public:
    double& operator[](size_t i) { if (i >= 4) throw std::out
…
total $64.47
2 * price $39.98, negated -$19.99
price < shipping: false, price == 19.99: true
reading[1] = 12
from functools import total_ordering

@total_ordering
class Money:
    def __init__(self, cents): self.cents = cents
    def __add__(self, other): return Money(self.cents + other.cents)
    def __mul__(self, n): return Money(self.cents * n)
    __rmul__ = __mul__                          # 3 * money
    def __eq__(self, other): return isinstance(other, Money) and self.cents == other.cents
    def __lt__(self, other): return self.cents < other.cents
    def __hash__(self): return hash(self.cents) # equal objects -> equal hashes
    def __repr__(self): return f"${self.cents // 100}.{self.cents % 100:02d}"

price = Money(1999)
print(price * 3 + Money(450), 2 * price, price >= Money(1999), len({Money(5), Money(5)}))
$64.47 $39.98 True 1

Step-by-Step Walkthrough

operator+= modifies and returns *this; operator+ takes its left operand by value, reuses +=, and returns the copy — the canonical pattern that keeps both consistent.

Binary operators are hidden friends (non-member functions defined inside the class), so conversions apply to both operands equally and 2 * price works as well as price * 2.

operator<=> defaulted generates all six comparisons, memberwise, so equality and ordering are consistent by construction.

operator[] has const and non-const versions: the const one returns const double& so const Readings can be read but not modified.

In Python, __rmul__ handles int * Money; __hash__ is defined because __eq__ is (otherwise instances become unhashable).

Common Mistakes

Surprising semantics: + that mutates its operand, == that compares only part of the state, << used for "add to list".

Inconsistent operators: a < b and b > a disagreeing, + and += implemented separately and drifting apart.

Asymmetric member operators: Money::operator*(int) as a member makes price * 2 work but 2 * price fail.

Overloading &&, || or , — loses short-circuit evaluation and sequencing.

Returning a reference to a local from operator+.

Trade-offs and Alternatives

If the meaning isn't obvious to a reader who has never seen the class, use a named method (schedule.merge(other) rather than schedule + other). Java and TypeScript force named methods, which is often clearer for non-mathematical types.

Real-World Use

std::string (+, ==, []), std::chrono durations (5s + 200ms), iterators (*, ++), smart pointers (->, *, bool), linear algebra libraries (Eigen), and money/decimal types.

Practice

Q1. (Easy) Which operators does a defaulted operator<=> let you use?

Answer: <, <=, >, >=; and a defaulted <=> also implicitly declares a defaulted == (hence !=).

Q2. (Medium) Why implement operator+ in terms of operator+=?

Answer: One source of truth: both always agree, and + reuses the (often more efficient) in-place logic.

Q3. (Advanced) Add prefix and postfix ++ to a Counter value type and show the difference.

#include <iostream>
class Counter {
    int v_ = 0;
public:
    Counter& operator++() { ++v_; return *this; }               // prefix: increment, return self
    Counter operator++(int) { Counter old = *this; ++v_; return old; }   // postfix: return old value
    int value() const { return v_; }
};
int main() {
    Counter c;
    Counter a = c++;     // a gets 0, c becomes 1
    Counter b = ++c;     // c becomes 2, b gets 2
    std::cout << a.value() << " " << b.value() << " " << c.value() << "\n";
}

Interview Question

Q1. "When should an operator be a member and when a non-member?"

Answer: Operators that must modify the left operand or need it to be the class (=, [], (), ->, and compound assignments +=) are members. Symmetric binary operators (+, *, ==, <, <<) are best as non-members (often hidden friends) so that implicit conversions apply to both operands and expressions like 2 * money compile. operator<< for streams must be a non-member because the left operand is std::ostream.

Quick Quiz

Q1. Can you change the precedence of + in C++?

Answer: No — arity, precedence and associativity are fixed.

Q2. Which Python method makes obj[i] work?

Answer: __getitem__ (and __setitem__ for assignment).

Q3. Does Java support user-defined operator overloading?

Answer: No.

Summary

Overload operators only for value types with obvious meaning.

Implement + via +=, make binary operators symmetric, default <=> for consistent comparisons.

Language support varies widely; Java and TypeScript use named methods.

Related: Module 12 (equality/hash), Module 18 (value objects).