← All Posts
C++ Deep Dives

Operator Overloading

What Is Operator Overloading?

C++ lets you redefine how operators (+, -, ==, <<, [], (), etc.) behave for your custom types. This is called operator overloading. It's one of C++'s most powerful features for creating expressive, readable code, but also one of the most misused when done without discipline.

How the Compiler Sees It

When you write a + b where a and b are your custom Vector2D objects, the compiler doesn't see a mathematical addition, it sees a function call. It translates the expression into one of two forms:

The compiler performs overload resolution just like any other function call: it considers all candidate operator+ functions in scope, finds the best match based on argument types (including implicit conversions), and calls that function. If no match is found, or if multiple matches are equally good, you get a compile error.

What You Can Overload

You can overload almost every C++ operator, including:

Why It Matters

Operator overloading makes your types feel like built-in types. Consider the difference:

// Without operator overloading:
Matrix result = add(multiply(A, B), C);

// With operator overloading:
Matrix result = A * B + C;

The second form reads like math. A BigInteger with == and < works seamlessly with std::sort. A smart pointer with * and -> behaves like a real pointer. A std::string with + lets you concatenate naturally.

This isn't just aesthetics, it enables generic programming. Templates like std::accumulate use + internally. If your type supports +, it automatically works with the algorithm. Without operator overloading, every algorithm would need customization hooks.

Golden rule: Only overload operators when they have an intuitive, obvious meaning for your type. + on a Vector makes sense. + on a Database does not. If you have to explain what your overloaded operator does, it's probably the wrong abstraction, use a named function instead.

Overloading Rules at a Glance

Member vs Free Function

You can define operator overloads in two fundamentally different ways, and the choice isn't just stylistic, it affects which conversions the compiler can apply and which expressions compile at all.

Member Function Form

The left operand is always *this. The compiler rewrites a + b as a.operator+(b):

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

    // Member: this + other
    Vec2 operator+(const Vec2& other) const {
        return Vec2(x + other.x, y + other.y);
    }
};

The key implication: the left operand must already be a Vec2. If someone writes 3.0 + myVec, the compiler won't try to convert 3.0 to a Vec2 to call the member function, it only searches for member functions on the left operand's type (which is double, not your class).

Free (Non-Member / Friend) Function Form

Both operands are parameters. The compiler rewrites a + b as operator+(a, b). Needed when the left operand isn't your class (e.g., std::ostream << yourObject):

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

    // Free function defined as friend for private access
    friend Vec2 operator+(const Vec2& a, const Vec2& b) {
        return Vec2(a.x + b.x, a.y + b.y);
    }
};

With a free function, implicit conversions can apply to both operands. If Vec2 has a constructor Vec2(double) that converts a scalar to a vector, then both myVec + 3.0 and 3.0 + myVec would work.

The friend Keyword

A friend declaration inside a class grants a non-member function access to private and protected members. You have three options for defining free-function operators:

// Option 1: friend defined inline (most common)
class Vec2 {
    double x, y;
public:
    friend Vec2 operator+(const Vec2& a, const Vec2& b) {
        return Vec2(a.x + b.x, a.y + b.y);  // can access x, y
    }
};

// Option 2: friend declared, defined outside
class Vec2 {
    double x, y;
public:
    friend Vec2 operator+(const Vec2& a, const Vec2& b);
};
Vec2 operator+(const Vec2& a, const Vec2& b) {
    return Vec2(a.x + b.x, a.y + b.y);
}

// Option 3: No friend — use public getters only
Vec2 operator+(const Vec2& a, const Vec2& b) {
    return Vec2(a.getX() + b.getX(), a.getY() + b.getY());
}

The Implicit Conversion Trap

Here's a concrete example of why the member vs free choice matters:

class Length {
    double meters_;
public:
    Length(double m) : meters_(m) {}  // implicit conversion from double

    // Member operator+
    Length operator+(const Length& rhs) const {
        return Length(meters_ + rhs.meters_);
    }
};

Length L(5.0);
Length a = L + 3.0;   // OK: 3.0 converts to Length for the rhs
Length b = 3.0 + L;   // ERROR: 3.0 is a double, no member operator+ on double

If operator+ were a free function, both lines would compile because implicit conversion can apply to either argument.

Decision Guide

OperatorMust BeReason
=, [], (), ->MemberLanguage requires it, these only make sense on the object itself
<<, >> (stream I/O)Free functionLeft operand is a stream, not your class
Arithmetic (+, -, *, /)Prefer freeAllows implicit conversions on both sides (symmetry)
Comparisons (==, <, etc.)Prefer freeSame symmetry reasoning; C++20 <=> can be a member
Compound assignment (+=, -=)Prefer memberModifies the left operand in-place; member is natural
Unary (!, -, ++)Prefer memberOnly one operand, the object itself

Arithmetic Operators

The four arithmetic operators (+, -, *, /) and their compound forms (+=, -=, etc.) are the most commonly overloaded. There's a canonical pattern that every C++ developer should know, and understanding why it works the way it does is crucial.

The Canonical Pattern: Compound First

The fundamental insight: implement += as a member function first, then define + as a free function that delegates to +=. This avoids code duplication, ensures consistency, and produces the most efficient code.

class Vec2 {
    double x, y;
public:
    Vec2(double x = 0, double y = 0) : x(x), y(y) {}

    // Step 1: Compound assignment (modifies in-place, returns reference)
    Vec2& operator+=(const Vec2& rhs) {
        x += rhs.x;
        y += rhs.y;
        return *this;  // return reference for chaining: a += b += c;
    }

    // Step 2: Binary + defined in terms of +=
    friend Vec2 operator+(Vec2 lhs, const Vec2& rhs) {
        lhs += rhs;   // lhs is a COPY (passed by value), so this is safe
        return lhs;
    }
};

Why This Pattern Works

Let's break down the design decisions line by line:

Unary Operators

Unary minus (negation) and unary plus are simple, they take no parameters and return a new object:

// Unary minus: -v
Vec2 operator-() const { return Vec2(-x, -y); }

// Unary plus (usually a no-op, but included for completeness)
Vec2 operator+() const { return *this; }

Scalar Multiplication

A common need is multiplying a vector by a scalar. You need two overloads for commutativity (v * 2.0 and 2.0 * v should both work):

// Vec2 * scalar
friend Vec2 operator*(const Vec2& v, double s) {
    return Vec2(v.x * s, v.y * s);
}
// scalar * Vec2 (delegates to the above)
friend Vec2 operator*(double s, const Vec2& v) {
    return v * s;
}

Implement one direction, then delegate from the other. This keeps the logic in one place and guarantees both directions produce the same result.

A Complete Example

Here's how a well-designed Vec2 class looks with the full arithmetic suite:

class Vec2 {
    double x, y;
public:
    Vec2(double x = 0, double y = 0) : x(x), y(y) {}

    // Compound assignments (member functions, return *this)
    Vec2& operator+=(const Vec2& rhs) { x += rhs.x; y += rhs.y; return *this; }
    Vec2& operator-=(const Vec2& rhs) { x -= rhs.x; y -= rhs.y; return *this; }
    Vec2& operator*=(double s)         { x *= s; y *= s; return *this; }
    Vec2& operator/=(double s)         { x /= s; y /= s; return *this; }

    // Binary operators (free functions, defined via compound operators)
    friend Vec2 operator+(Vec2 a, const Vec2& b) { return a += b; }
    friend Vec2 operator-(Vec2 a, const Vec2& b) { return a -= b; }
    friend Vec2 operator*(Vec2 v, double s)       { return v *= s; }
    friend Vec2 operator*(double s, const Vec2& v) { return Vec2(v) *= s; }
    friend Vec2 operator/(Vec2 v, double s)       { return v /= s; }

    // Unary operators
    Vec2 operator-() const { return Vec2(-x, -y); }
    Vec2 operator+() const { return *this; }
};

Notice how concise the binary operators are when they delegate to compound assignments. Each is a one-liner. All the actual logic lives in +=, -=, *=, /=.

Common mistake: Returning const Vec2 from operator+. Some older guides recommend this to prevent (a + b) = c, but it defeats move semantics in modern C++. Just return by value.

▶ Vector Addition Animation

Watch operator+ add two Vec2 objects component-wise.

Comparison Operators

Comparisons are the second most commonly overloaded operator category. They're essential for using your types with STL containers (std::set, std::map) and algorithms (std::sort, std::binary_search). C++20 revolutionized this area with the spaceship operator, but understanding the old way is important for maintaining legacy code.

Pre-C++20: The Verbose Way

Before C++20, you had to write up to six separate operator functions: ==, !=, <, >, <=, >=. The canonical pattern reduces this to just two implementations:

class Fraction {
    int num, den;
public:
    Fraction(int n, int d) : num(n), den(d) {}

    // Core: operator== and operator<
    friend bool operator==(const Fraction& a, const Fraction& b) {
        return (long long)a.num * b.den == (long long)b.num * a.den;
    }
    friend bool operator<(const Fraction& a, const Fraction& b) {
        return (long long)a.num * b.den < (long long)b.num * a.den;
    }

    // Derived: all others defined in terms of == and <
    friend bool operator!=(const Fraction& a, const Fraction& b) {
        return !(a == b);
    }
    friend bool operator>(const Fraction& a, const Fraction& b) {
        return b < a;        // flip operands
    }
    friend bool operator<=(const Fraction& a, const Fraction& b) {
        return !(b < a);     // not greater
    }
    friend bool operator>=(const Fraction& a, const Fraction& b) {
        return !(a < b);     // not less
    }
};

This pattern, implementing == and <, then deriving the rest, keeps the real logic in two places. But it's still six functions and a lot of boilerplate. Enter C++20.

C++20: The Spaceship Operator (<=>)

The three-way comparison operator <=> (nicknamed "spaceship" because <=> looks like a UFO) compares two values and returns an ordering result that encodes whether the left operand is less than, equal to, or greater than the right. From this single function, the compiler synthesizes all six comparison operators.

Ordering Types

The return type of <=> tells the compiler what kind of ordering your type has. This matters because not all types can be compared in the same way:

Return TypeMeaningExample Types
std::strong_orderingExactly one of <, ==, > is true. Equal objects are indistinguishable (substitutable).int, char, std::string
std::weak_orderingSame three-way split, but equivalent objects may be distinguishable. Two values can compare as "equivalent" without being truly equal.Case-insensitive string comparison
std::partial_orderingSome values may be incomparable (neither less, greater, nor equal).float/double (because of NaN)

For most types, std::strong_ordering is what you want. Use std::partial_ordering only when your type has values that genuinely can't be compared (like floating-point NaN).

The Simplest Case: Defaulted Spaceship

#include <compare>

class Point {
    int x, y;
public:
    Point(int x, int y) : x(x), y(y) {}

    // Compiler generates ALL comparisons: ==, !=, <, >, <=, >=
    auto operator<=>(const Point&) const = default;
};

When you = default the spaceship operator, the compiler does a member-by-member comparison in declaration order. For Point, it first compares x, and if they're equal, compares y. This is lexicographic ordering, exactly what you'd typically want for a struct.

The auto return type lets the compiler deduce the strongest ordering category that all members support.

Custom Spaceship

When the default member-by-member comparison isn't right, write the body yourself:

class Fraction {
    int num, den;
public:
    Fraction(int n, int d) : num(n), den(d) {}

    // Custom three-way comparison
    std::strong_ordering operator<=>(const Fraction& rhs) const {
        // Cross-multiply to compare without division
        auto lhs_val = (long long)num * rhs.den;
        auto rhs_val = (long long)rhs.num * den;
        return lhs_val <=> rhs_val;  // delegates to built-in <=> on long long
    }

    // With custom <=>, you must ALSO define == separately
    bool operator==(const Fraction& rhs) const {
        return num * rhs.den == rhs.num * den;
    }
};

Important subtlety: When you write a custom (non-defaulted) <=>, the compiler only synthesizes <, >, <=, >= from it. It does not synthesize == and !=, you must define operator== separately. Why? Because equality checking is often more efficient than full three-way comparison (e.g., for strings, you can first check length before comparing characters).

How the Compiler Uses <=>

When you write a < b and there's no direct operator<, the compiler rewrites it as (a <=> b) < 0. If that doesn't work, it tries 0 < (b <=> a) (reversed). This means a single <=> definition enables all four relational operators to work in both directions.

Tip: For simple structs, auto operator<=>(const T&) const = default; does member-by-member comparison in declaration order. Write the body only when you need custom logic (like the Fraction cross-multiplication above).

Stream Operators (<< and >>)

Overloading << for std::ostream lets your objects work with std::cout, std::ostringstream, file streams, and any output stream. This is arguably the most practical operator to overload, you'll use it constantly during debugging and logging.

Why They Must Be Free (Friend) Functions

This is one of the most-asked questions in C++ interviews and one of the most important design constraints to understand deeply. Let's walk through the reasoning step by step.

Step 1: How the Compiler Resolves Member Operators

When the compiler sees a << b, it tries two things:

  1. a.operator<<(b), look for a member function on the type of a
  2. operator<<(a, b), look for a free function with matching parameters

For member functions, the left operand is always *this. That means the class of the left operand is where the member function must live.

Step 2: Who Owns the Left Operand?

Now consider std::cout << myVec. The left operand is std::cout, which is of type std::ostream. If you tried to define operator<< as a member function, it would have to be a member of std::ostream:

// IMPOSSIBLE: You'd need to modify the standard library itself!
class std::ostream {
public:
    std::ostream& operator<<(const Vec2& v) {  // Can't add this!
        return *this << "(" << v.x << ", " << v.y << ")";
    }
};

You can't modify std::ostream, it's part of the standard library. The headers are read-only. Even if you could, every user-defined type in the world would need to add itself into std::ostream's definition, which is absurd.

Step 3: Could We Make It a Member of Our Class Instead?

What if we made it a member of Vec2? Then the left operand would be *this (a Vec2):

class Vec2 {
public:
    // This compiles, but the syntax is BACKWARDS!
    std::ostream& operator<<(std::ostream& os) const {
        return os << "(" << x << ", " << y << ")";
    }
};

// Usage would be:
myVec << std::cout;   // Works but reads horribly
// std::cout << myVec; // DOESN'T COMPILE! cout has no member for Vec2

The syntax is reversed, myVec << std::cout instead of std::cout << myVec. Nobody writes it that way. It breaks chaining: std::cout << a << b won't work because the first << returns an ostream& and the second << has the stream on the left again.

Step 4: The Only Solution: Free Function

Since we can't add a member to std::ostream and making it a member of our class reverses the syntax, the only option is a free function where both operands are parameters:

// Free function: compiler sees  operator<<(std::cout, myVec)
std::ostream& operator<<(std::ostream& os, const Vec2& v) {
    return os << "(" << v.x << ", " << v.y << ")";
}

But there's a problem: x and y are private members of Vec2. A free function can't access them. We have two choices:

Step 5: Why friend Is the Canonical Choice

class Vec2 {
    double x, y;   // private!
public:
    Vec2(double x, double y) : x(x), y(y) {}

    // Declared as friend: it's a FREE function with private access
    friend std::ostream& operator<<(std::ostream& os, const Vec2& v) {
        return os << "(" << v.x << ", " << v.y << ")";
    }
};

friend here does two things simultaneously:

  1. Declares it as a non-member function, it's not a member of Vec2, so the left operand can be anything (here, std::ostream&).
  2. Grants private access, the function body can directly use v.x and v.y without public getters.
To summarize: Stream operators must be free functions because the left operand (std::ostream / std::istream) is a standard library type you can't modify. They're declared as friend because they typically need access to private data. This isn't a design choice, it's a constraint imposed by how C++ resolves operator calls.

The Canonical Pattern

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

    // Output: cout << myVec prints "(3.5, 2.1)"
    friend std::ostream& operator<<(std::ostream& os, const Vec2& v) {
        return os << "(" << v.x << ", " << v.y << ")";
    }

    // Input: cin >> myVec reads two doubles
    friend std::istream& operator>>(std::istream& is, Vec2& v) {
        is >> v.x >> v.y;
        if (!is) v = Vec2(0, 0);  // reset on failure
        return is;
    }
};

Key Design Decisions

Formatting with I/O Manipulators

For more complex formatting, be aware that your operator inherits whatever formatting state the stream currently has:

// If someone does: cout << std::fixed << std::setprecision(2) << myVec;
// Your operator will print (3.50, 2.10) because fixed/precision are sticky.

// If you need specific formatting, save and restore:
friend std::ostream& operator<<(std::ostream& os, const Vec2& v) {
    auto flags = os.flags();      // save
    auto prec = os.precision();
    os << std::fixed << std::setprecision(3)
       << "(" << v.x << ", " << v.y << ")";
    os.flags(flags);              // restore
    os.precision(prec);
    return os;
}
Real-world usage: Every major C++ library overloads <<. Boost, Eigen, Qt, they all provide stream operators for their types. It's expected for any class that represents a "value."

Subscript Operator ([])

The subscript operator lets your object be indexed like an array or looked up like a dictionary. It must be a member function (language requirement). You should always provide both const and non-const overloads to support read-only and read-write access.

Basic Pattern: Const and Non-Const

class Matrix {
    std::vector<std::vector<double>> data_;
    size_t rows_, cols_;
public:
    Matrix(size_t r, size_t c)
        : data_(r, std::vector<double>(c)), rows_(r), cols_(c) {}

    // Non-const: allows modification (mat[2][3] = 4.5)
    std::vector<double>& operator[](size_t row) {
        return data_[row];
    }

    // Const: read-only access (works on const Matrix objects)
    const std::vector<double>& operator[](size_t row) const {
        return data_[row];
    }
};

Matrix mat(3, 4);
mat[2][3] = 4.5;              // non-const version
const Matrix& ref = mat;
double val = ref[2][3];       // const version

Why Both Versions Matter

Without the const overload, you couldn't use your class through a const reference or in any const context (like a function that takes const Matrix&). Without the non-const overload, you couldn't assign through the subscript.

The compiler chooses the right overload based on whether the object is const:

void readOnly(const Matrix& m) {
    double v = m[0][0];   // calls the const overload
    // m[0][0] = 1;       // ERROR: const overload returns const reference
}

void readWrite(Matrix& m) {
    m[0][0] = 1;          // calls the non-const overload
}

Bounds Checking

The built-in [] operator on arrays doesn't check bounds (for performance). You can choose to add bounds checking in your overload. A common pattern is to provide both unchecked [] and checked at():

class SafeArray {
    std::vector<int> data_;
public:
    int& operator[](size_t i)       { return data_[i]; }          // fast, unchecked
    const int& operator[](size_t i) const { return data_[i]; }    // fast, unchecked

    int& at(size_t i) {                                            // checked
        if (i >= data_.size()) throw std::out_of_range("index");
        return data_[i];
    }
};

C++23: Multi-Dimensional Subscript

Before C++23, operator[] could only take a single argument. To emulate mat[2][3] you had to return a proxy object (like a row reference). C++23 lifts this restriction:

// C++23: operator[] can take multiple arguments
class Matrix {
    std::vector<double> data_;
    size_t cols_;
public:
    double& operator[](size_t row, size_t col) {
        return data_[row * cols_ + col];
    }
};

Matrix m(3, 4);
m[2, 3] = 4.5;   // Direct multi-dimensional access!

This is cleaner than the proxy pattern and has zero overhead. Until C++23, the () operator was the idiomatic alternative for multi-dimensional access (e.g., Eigen uses mat(2, 3)).

Function Call Operator (())

Overloading () makes your object callable like a function. These objects are called functors (or function objects). They're a core C++ idiom that predates lambdas and remains relevant in modern code.

Basic Functor

class Adder {
    int value_;
public:
    explicit Adder(int v) : value_(v) {}

    int operator()(int x) const {
        return x + value_;
    }
};

Adder add5(5);
int result = add5(10);  // result = 15  — add5 looks and acts like a function!

Why Functors Exist

The key advantage over plain function pointers: functors can carry state. A function pointer is just an address, it can't remember anything between calls. A functor is an object with member variables:

// Function pointer: no state
int addFive(int x) { return x + 5; }  // "5" is hardcoded

// Functor: carries state
Class Adder {
    int value_;
public:
    Adder(int v) : value_(v) {}  // "v" can be anything
    int operator()(int x) const { return x + value_; }
};

// You can create different adders at runtime:
Adder add3(3);
Adder add100(100);
add3(7);    // 10
add100(7);  // 107

Functors with STL Algorithms

Functors shine when used with algorithms. The STL was designed around them:

// Custom comparator as a functor
struct CaseInsensitiveLess {
    bool operator()(const std::string& a, const std::string& b) const {
        return std::lexicographical_compare(
            a.begin(), a.end(), b.begin(), b.end(),
            [](char c1, char c2) { return tolower(c1) < tolower(c2); }
        );
    }
};

// Use it with std::set for case-insensitive ordering
std::set<std::string, CaseInsensitiveLess> names;
names.insert("Alice");
names.insert("alice");  // treated as duplicate!

// Use it with std::sort
std::vector<std::string> words = {"Banana", "apple", "Cherry"};
std::sort(words.begin(), words.end(), CaseInsensitiveLess());
// words = {"apple", "Banana", "Cherry"}

Functors vs Lambdas

C++11 lambdas are actually syntactic sugar for anonymous functors. The compiler generates a class with operator() for each lambda:

// This lambda:
auto add5 = [](int x) { return x + 5; };

// Is roughly equivalent to:
struct __lambda_1 {
    int operator()(int x) const { return x + 5; }
};
auto add5 = __lambda_1{};

// Capturing variables creates member variables:
int n = 5;
auto addN = [n](int x) { return x + n; };

// Is roughly:
struct __lambda_2 {
    int n;
    int operator()(int x) const { return x + n; }
};
auto addN = __lambda_2{5};

When to still use explicit functors over lambdas:

Dereference & Arrow Operators (*, ->)

Overloading * (dereference) and -> (member access) is how smart pointers, iterators, and proxy objects work. They make your wrapper types behave like raw pointers, which is essential for generic code that doesn't care whether it's working with a raw pointer, a unique_ptr, or a custom handle.

The Dereference Operator (*)

This is a unary prefix operator that returns a reference to the pointed-to object:

template<typename T>
class SmartPtr {
    T* ptr_;
public:
    explicit SmartPtr(T* p) : ptr_(p) {}
    ~SmartPtr() { delete ptr_; }

    // Dereference: *sp returns the object
    T& operator*() const { return *ptr_; }

    // Arrow: sp->member accesses the object's members
    T* operator->() const { return ptr_; }
};

SmartPtr<Widget> w(new Widget());
(*w).doWork();   // operator* returns Widget&, then call doWork()
w->doWork();     // operator-> returns Widget*, compiler then accesses doWork()

How operator-> Is Special

The arrow operator has unique behavior in C++, the compiler chains calls until it gets a raw pointer. When you write w->doWork():

  1. The compiler calls w.operator->(), which returns T* (a raw pointer).
  2. The compiler then applies -> to that raw pointer: ptr_->doWork().

If operator-> returns another object (not a raw pointer), the compiler calls operator-> on that object, and so on, until it reaches a raw pointer. This chaining enables proxy patterns:

// Proxy that logs access
struct LoggingProxy {
    Widget* target_;
    LoggingProxy(Widget* t) : target_(t) { std::cout << "Accessing widget\n"; }
    Widget* operator->() { return target_; }
    ~LoggingProxy() { std::cout << "Done accessing\n"; }
};

class LoggingPtr {
    Widget* ptr_;
public:
    LoggingProxy operator->() { return LoggingProxy(ptr_); }
    // w->doWork() calls:
    //   1. LoggingPtr::operator->() → returns LoggingProxy
    //   2. LoggingProxy::operator->() → returns Widget*
    //   3. Widget*->doWork()
};

Const Correctness

Notice that operator* and operator-> are const member functions that return non-const references/pointers. This matches raw pointer semantics: a const pointer (Widget* const p) can't be reassigned, but you can still modify the object it points to. If you want to propagate constness (a const SmartPtr should give read-only access), you need separate const and non-const overloads:

T& operator*()              { return *ptr_; }  // mutable access
const T& operator*() const  { return *ptr_; }  // read-only access
T* operator->()             { return ptr_; }
const T* operator->() const { return ptr_; }

std::unique_ptr and std::shared_ptr do not propagate constness (matching raw pointer behavior). C++20's std::experimental::propagate_const wrapper exists for when you want this.

Increment & Decrement (++, --)

Incrementing and decrementing is common for iterators, counters, date types, and any type that represents a position in a sequence. The tricky part is distinguishing prefix (++x) from postfix (x++), they have the same operator symbol but different semantics and different signatures.

The Dummy int Convention

C++ uses a clever (if inelegant) hack: postfix versions take a dummy int parameter that's never used. It's purely a syntactic marker to distinguish the two forms:

class Counter {
    int value_;
public:
    explicit Counter(int v = 0) : value_(v) {}

    // Prefix: ++c (increment FIRST, return NEW value)
    Counter& operator++() {
        ++value_;
        return *this;  // return reference to modified object
    }

    // Postfix: c++ (save OLD value, increment, return OLD)
    Counter operator++(int) {   // <-- dummy int = postfix marker
        Counter old = *this;    // save a copy of current state
        ++value_;               // increment
        return old;             // return the OLD copy
    }

    int get() const { return value_; }
};

Why Prefix Is More Efficient

Look at the implementations carefully:

This is why the C++ community convention is: prefer prefix when you don't need the old value. In a for loop, ++i is preferred over i++ (though for int, modern compilers generate identical code).

Implementing One in Terms of the Other

The canonical approach: implement prefix, then define postfix using prefix:

// Prefix: the "real" implementation
Counter& Counter::operator++() {
    ++value_;
    return *this;
}

// Postfix: delegates to prefix
Counter Counter::operator++(int) {
    Counter old = *this;   // copy
    ++(*this);             // use PREFIX to do the actual increment
    return old;
}

This way, the increment logic lives in one place (prefix). If you later change what "increment" means (e.g., skip weekends for a Date class), you only update one function.

Iterator Example

Iterators are the most common real-world use of overloaded ++:

template<typename T>
class LinkedListIterator {
    Node<T>* current_;
public:
    // Prefix: advance and return
    LinkedListIterator& operator++() {
        current_ = current_->next;
        return *this;
    }

    // Postfix: return old position, then advance
    LinkedListIterator operator++(int) {
        LinkedListIterator old = *this;
        ++(*this);
        return old;
    }

    T& operator*() const { return current_->data; }
    bool operator!=(const LinkedListIterator& other) const {
        return current_ != other.current_;
    }
};

// Usage with range-based for:
for (auto it = list.begin(); it != list.end(); ++it) {
    std::cout << *it << "\n";
}

▶ Prefix vs Postfix Increment

Watch how prefix returns the new value while postfix returns the old value.

Conversion Operators

Conversion operators let your type automatically (or explicitly) convert to another type. They're the inverse of converting constructors: a constructor converts from another type to yours; a conversion operator converts from yours to another type.

Syntax

Conversion operators have no return type in their declaration, the return type is the type you're converting to:

class Percentage {
    double value_;
public:
    explicit Percentage(double v) : value_(v) {}

    // Convert to double: value/100
    explicit operator double() const { return value_ / 100.0; }

    // Convert to bool: is non-zero?
    explicit operator bool() const { return value_ != 0.0; }
};

Implicit vs Explicit

This is the most important decision for conversion operators. Always use explicit unless you have a very good reason not to:

class BadPercentage {
    double value_;
public:
    BadPercentage(double v) : value_(v) {}
    operator double() const { return value_ / 100.0; }  // IMPLICIT — dangerous!
};

BadPercentage p(75);
double x = p;         // OK: implicit conversion to 0.75
double y = p + 1.0;   // "Surprise!" — p converts to 0.75, not 75 + 1.0
// Was the programmer expecting 0.75 + 1.0 = 1.75? Or 76.0?
// Implicit conversions create ambiguity.

// With explicit:
class GoodPercentage {
    double value_;
public:
    explicit operator double() const { return value_ / 100.0; }
};

GoodPercentage p(75);
// double x = p;              // ERROR: explicit conversion required
double x = static_cast<double>(p);  // OK: programmer clearly intended this
// double y = p + 1.0;        // ERROR: no implicit conversion

The Special Case of operator bool()

explicit operator bool() is special: even though it's explicit, it can be used in boolean contexts without a cast. This is called "contextual conversion" and applies in:

class Connection {
public:
    explicit operator bool() const { return isOpen(); }
};

Connection conn = connect();
if (conn) { /* works — contextual conversion to bool */ }
bool b = conn;            // ERROR: not a boolean context
bool b = static_cast<bool>(conn);  // OK: explicit cast

This is how std::optional, std::unique_ptr, std::shared_ptr, and stream objects work in if statements. They all have explicit operator bool().

The Classic Safe Bool Problem

Before C++11, there was no explicit keyword for conversion operators. Implicit operator bool() caused infamous bugs:

// Pre-C++11: implicit operator bool
class OldStream {
public:
    operator bool() const { return good(); }  // implicit!
};

OldStream a, b;
int x = a + b;   // Compiles! bool converts to int. a + b = 0, 1, or 2.
a << 3;          // Compiles! bool converts to int, then shifts. Nonsense.
if (a == b) {}   // Compiles! Compares bools, not stream states.
// None of these make sense, but they all compile silently.

The pre-C++11 workaround was the "safe bool idiom" (converting to a pointer-to-member instead of bool). C++11's explicit operator bool made all of that unnecessary.

Rule of thumb: Use explicit on all conversion operators. The only exception is rare cases where implicit conversion is truly natural and unambiguous (which is almost never).

Best Practices & Common Pitfalls

Do's

Don'ts

The Copy-and-Swap Idiom

For operator= (assignment), the copy-and-swap idiom provides strong exception safety and handles self-assignment automatically:

class String {
    char* data_;
    size_t size_;
public:
    // Copy assignment using copy-and-swap
    String& operator=(String other) {  // note: passed BY VALUE (makes a copy)
        swap(*this, other);            // swap our guts with the copy's guts
        return *this;
    }                                  // old data destroyed when 'other' goes out of scope

    friend void swap(String& a, String& b) noexcept {
        using std::swap;
        swap(a.data_, b.data_);
        swap(a.size_, b.size_);
    }
};

If the copy (parameter passing) throws, the original object is untouched. If it succeeds, the swap is noexcept, so the assignment either fully succeeds or doesn't happen at all.

Operators You Cannot Overload

These operators are fixed by the language and cannot be user-defined, no matter what:

OperatorNameWhy It Can't Be Overloaded
::Scope resolutionResolved at compile time; operates on names, not values
.Member accessMust reliably access the actual member; overloading would break the ability to reach any member
.*Pointer-to-memberSame reason as .
?:Ternary conditionalRelies on short-circuit evaluation (only evaluates one branch)
sizeofSize queryCompile-time operator; not a function call
alignofAlignment querySame — compile-time intrinsic
typeidType identificationRuntime type info; fundamental language mechanism

Also note: you cannot create entirely new operators (like ** for exponentiation or <- for assignment). You can only overload existing C++ operators. The operator symbol, precedence, and associativity are all fixed.

Quick Reference

This table summarizes the canonical way to define each operator category:

OperatorDefine AsReturnsNotes
+, -, *, /Free function (friend)By valueDefine via compound assignment. Take lhs by value.
+=, -=, etc.MemberReference to *thisContains the real logic. Compound first, binary second.
==, <=>Member or friendbool / orderingC++20: one <=> generates all six. Must define == separately for custom <=>.
<<, >>Free function (friend)Stream referenceLeft operand is the stream. Return it for chaining.
[]MemberReferenceProvide const and non-const overloads. C++23: multi-arg.
()MemberAnythingMakes functors. Can have multiple overloads with different params.
*, ->MemberReference / pointerArrow chains until raw pointer. Used for smart pointers & iterators.
++ (prefix)MemberReference to *thisNo parameter. Modifies in-place. More efficient than postfix.
++ (postfix)Member (dummy int)By value (old copy)Dummy int parameter. Returns copy of old state.
operator T()MemberImplicit in nameAlways use explicit. explicit operator bool works in if/while.
=MemberReference to *thisConsider copy-and-swap for exception safety.
One-sentence summary: Operator overloading lets your types participate in C++'s expression syntax. Use it to make your types feel built-in, but only when the meaning is obvious. When in doubt, write a named function.