Skip to content
C++
Language
since C++11
Basic

Master Operator Overloading to Build Expressive C++ Types

Learn to overload operators so your custom types support +, ==, <<, and more — and understand when overloading helps versus hurts readability.

By the end of this page, you will be able to overload arithmetic, comparison, and stream operators for your own types; choose between member and non-member implementations; avoid the classic pitfalls that produce surprising behavior; and use C++20's spaceship operator to generate a full family of comparisons from a single declaration.


What and Why

When you write 3 + 4, the + sign is just syntax sugar for a built-in operation the compiler already knows about. Your own Vec2 or Fraction type doesn't get that for free — but C++ lets you teach the compiler what +, ==, <<, and most other operators mean for your type.

That's operator overloading: defining a function whose name starts with operator followed by the symbol you want to give meaning to.

The payoff is expressive code:

cpp
// Without overloading
Vec2 result = add(scale(a, 2.0f), b);

// With overloading
Vec2 result = a * 2.0f + b;

Both do the same work, but the second reads like the math it represents. The rule to keep in mind: overload an operator only when the meaning is obvious and unsurprising to any reader. Matrix * Matrix is fine. Thread * Thread is not.


Step by Step

A minimal value type

Start with a simple two-dimensional vector. It holds x and y and needs to support addition.

cpp
// Requires C++11
#include <iostream>

struct Vec2 {
    float x, y;
};

int main() {
    Vec2 a{1.0f, 2.0f};
    Vec2 b{3.0f, 4.0f};
    // Vec2 c = a + b;  // won't compile yet
}

Adding operator+ as a member function

The simplest form: define the operator inside the class. The left-hand operand is *this; the right-hand operand arrives as a parameter.

cpp
#include <iostream>

struct Vec2 {
    float x, y;

    Vec2 operator+(const Vec2& rhs) const {
        return Vec2{x + rhs.x, y + rhs.y};
    }
};

int main() {
    Vec2 a{1.0f, 2.0f};
    Vec2 b{3.0f, 4.0f};
    Vec2 c = a + b;
    std::cout << c.x << ' ' << c.y << '\n'; // 4 6
}

The const at the end of the member signature is important: it promises that operator+ won't modify the left-hand operand — which is correct, since a + b should never change a.

Adding operator+= first, then deriving operator+

A common and clean pattern: implement the compound assignment form first (it modifies *this), then build the binary form on top of it.

cpp
#include <iostream>

struct Vec2 {
    float x, y;

    Vec2& operator+=(const Vec2& rhs) {
        x += rhs.x;
        y += rhs.y;
        return *this;
    }
};

// Free function: lives outside the class
inline Vec2 operator+(Vec2 lhs, const Vec2& rhs) {
    lhs += rhs;   // reuse the member
    return lhs;   // lhs is a copy, so this is safe
}

int main() {
    Vec2 a{1.0f, 2.0f};
    Vec2 b{3.0f, 4.0f};
    a += b;
    std::cout << a.x << ' ' << a.y << '\n'; // 4 6
}

Notice lhs is taken by value (a copy), so mutating it inside the free function doesn't touch the caller's a.

Comparison with == and the C++20 spaceship operator

Before C++20, you had to write every comparison operator by hand. With C++20 you declare one three-way comparison and the compiler generates ==, !=, <, <=, >, and >= for you.

cpp
// Requires C++20
#include <compare>
#include <iostream>

struct Vec2 {
    float x, y;

    auto operator<=>(const Vec2&) const = default;
    bool operator==(const Vec2&) const = default;
};

int main() {
    Vec2 a{1.0f, 2.0f};
    Vec2 b{1.0f, 2.0f};
    Vec2 c{3.0f, 4.0f};

    std::cout << (a == b) << '\n'; // 1
    std::cout << (a == c) << '\n'; // 0
}

= default tells the compiler to perform a member-by-member comparison in declaration order. For types where that is the right semantics, this is the preferred approach in C++20.

Stream output with operator<<

std::cout << myVec requires operator<< to be a free function (or a friend), because the left-hand side is std::ostream, which you don't own.

cpp
#include <iostream>

struct Vec2 {
    float x, y;
};

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

int main() {
    Vec2 a{1.0f, 2.0f};
    std::cout << a << '\n'; // (1, 2)
}

The return type std::ostream& is what makes std::cout << a << '\n' work — without it, the second << would have nothing to chain to.


Common Patterns

Symmetric arithmetic via non-member functions

When an operator is commutative (order doesn't matter), a free function makes both orderings work equally:

cpp
#include <iostream>

struct Vec2 {
    float x, y;
    Vec2& operator*=(float s) { x *= s; y *= s; return *this; }
};

inline Vec2 operator*(Vec2 v, float s) { v *= s; return v; }
inline Vec2 operator*(float s, Vec2 v) { return v * s; } // reuse above

int main() {
    Vec2 a{1.0f, 2.0f};
    Vec2 b = a * 3.0f; // works
    Vec2 c = 3.0f * a; // also works
    std::cout << b.x << ' ' << c.x << '\n'; // 3 3
}

A member operator* can only handle vec * scalar. The extra free function flips the arguments.

Index operator for container-like types

operator[] gives your type array-style access. Return a reference so callers can both read and write:

cpp
#include <array>
#include <stdexcept>

struct Vec4 {
    std::array<float, 4> data{};

    float& operator[](std::size_t i) {
        if (i >= 4) throw std::out_of_range("Vec4 index");
        return data[i];
    }
    float operator[](std::size_t i) const {
        if (i >= 4) throw std::out_of_range("Vec4 index");
        return data[i];
    }
};

int main() {
    Vec4 v;
    v[0] = 1.0f;
    v[1] = 2.0f;
}

Two overloads: one for mutable access, one for const objects (returns by value so nobody can modify a const Vec4 through an index).

Unary negation

cpp
#include <iostream>

struct Vec2 {
    float x, y;
    Vec2 operator-() const { return Vec2{-x, -y}; }
};

int main() {
    Vec2 a{1.0f, -2.0f};
    Vec2 b = -a;
    std::cout << b.x << ' ' << b.y << '\n'; // -1 2
}

What Can Go Wrong

Returning *this from binary operators. a + b should produce a new value, not modify a. Returning *this by reference is correct for +=, -=, etc., but wrong for +, -.

cpp
// WRONG
Vec2& operator+(const Vec2& rhs) { x += rhs.x; y += rhs.y; return *this; }

// CORRECT
Vec2 operator+(const Vec2& rhs) const { return Vec2{x + rhs.x, y + rhs.y}; }

Forgetting const correctness. If operator+ is not const, you can't add two const Vec2 values. Mark read-only operators const.

Overloading && or || and breaking short-circuit evaluation. The built-in && and || don't evaluate the right operand if the left already determines the result. An overloaded version is a normal function call — both sides always evaluate. Avoid overloading these unless you have a very specific reason.

Missing the return from operator<<. Forgetting return os; makes chaining (cout << a << b) fail to compile. Always return the stream reference.


Quick Reference

Operator categoryTypical formNotes
Arithmetic (+, -, *, /)Free function, takes LHS by valueImplement += first, derive + from it
Compound assignment (+=, -=)Member, returns *this by refModifies *this in place
Comparison (C++11)Free function or memberMust write each one; keep them consistent
Three-way comparison (C++20)Member <=>, = defaultGenerates all six comparisons automatically
Stream output (<<)Free function or friend, returns ostream&Must return stream for chaining
Index ([])Member (two overloads: mutable + const)Return reference for mutable, value for const
Unary (-, !, ~)Member, returns by valueDoes not modify *this
Conversion (operator T())Member, usually explicitMark explicit to prevent surprise conversions

Member vs. free function rule of thumb:

  • Use a member when the left-hand operand must be your type ([], (), ->, =, compound assignments).
  • Use a free function when you want symmetry or the left-hand operand could be a different type (<<, >>, commutative arithmetic).

What's Next

  • Operator Precedence — your overloaded operators inherit built-in precedence rules; knowing them prevents subtle bugs.
  • The Spaceship Operator — deep dive into operator<=>, comparison categories, and how the compiler synthesises the six comparisons.
  • Function Overloading — operator overloading is a special case of function overloading; understanding the general rules helps resolve ambiguities.
  • Operator Overloading Reference — exhaustive list of every overloadable operator, its signature requirements, and which can be defaulted.