cs.thefarshad
medium

Operator Overloading

Give your own types natural syntax — define operator+, ==, <<, and [] as member or free functions, and know when not to.

Operators in C++ are just functions with special names. When you write a + b for two objects of a custom type, the compiler rewrites it into a call to a function named operator+. Overloading these lets your types read like built-in ones — a Vector2, a Matrix, or a BigInt can use +, ==, and << directly. Step through the expression below to watch each operator desugar into its underlying call.

> Vec2 a{1, 2}, b{3, 4};
Vec2 c = a + b;
bool same = (c == b);
double m = c[0];
std::cout << c;
Vec2 a{1, 2}, b{3, 4};// two Vec2 values constructed
kind: member
a
{ x: 1, y: 2 }
b
{ x: 3, y: 4 }
c
std::cout
(no output yet)
1/5
Two Vec2 objects exist. Operators are just functions named operator@; the compiler picks one by the operand types.

Member versus free functions

You can write an operator two ways. As a member function, the left operand is the implicit this; as a free (non-member) function, both operands are explicit parameters and the call is symmetric.

struct Vec2 {
  double x, y;

  // Member: left operand is *this. Good for []  and compound assignment.
  Vec2& operator+=(const Vec2& rhs) {
      x += rhs.x; y += rhs.y;
      return *this;
  }
  double operator[](int i) const { return i == 0 ? x : y; }
};

The rule of thumb: make it a member when it modifies the left operand (+=, =, [], ()), and a free function when both operands should be treated equally (+, ==, <). A common pattern is to write the compound form (+=) as a member and define the binary form (+) as a free function that reuses it. Symmetric free functions also allow conversions on the left operand, so 2.0 * v can work as well as v * 2.0.

The stream insertion operator

operator<< must be a free function, because its left operand is a std::ostream& — a type you do not own and cannot add members to:

#include <ostream>

std::ostream& operator<<(std::ostream& os, const Vec2& v) {
    return os << '(' << v.x << ", " << v.y << ')';   // return the stream
}

std::cout << v << '\n';   // -> operator<<(std::cout, v)

Returning the stream by reference is what makes chaining (os << a << b) work: each call hands the stream back to the next.

When not to overload

Overloading is for operations that are genuinely arithmetic, comparison, or indexing on your type. Keep the meaning intuitive+ should combine, not delete. Do not overload &&, ||, or , (you lose short-circuit and sequencing semantics), and never surprise the reader. Since C++20, defaulting operator<=> (the three-way “spaceship” comparison) and operator== generates a full, correct set of comparisons for you, so you rarely hand-write the six relational operators anymore.

Takeaways

  • a @ b compiles to a call to operator@; overloading gives custom types natural syntax.
  • Make operators that modify the left operand members (+=, [], =); make symmetric ones free functions (+, ==).
  • operator<< must be a free function taking std::ostream& and should return the stream for chaining.
  • Reuse: define + in terms of a member += to avoid duplicate logic.
  • In C++20, default operator<=> and operator== instead of writing all comparisons by hand; never overload &&, ||, or ,.

References