Operator Overloading  «Prev  Next»
Lesson 7 Unary and binary operator overloading
Objective Understand correct signatures, when to prefer member or non-member, and how to avoid surprise.

Unary vs. Binary Operator Overloading in Modern C++

Quick critique of prior version (why this rewrite?):
  • Examples mixed member-only guidance with outdated patterns (void main, broad using namespace std;), and missed the crucial difference between prefix and postfix signatures for ++/--.
  • Unrelated “static storage” and member-pointer digressions distracted from the core unary/binary topic.
  • No emphasis on return-by-reference where expected (+=, pre-increment) or on when to favor non-member overloads for symmetry.
This rewrite focuses on semantic correctness, minimal surprise, and modern guidance aligned with our site’s operator-overloading modules.

Core ideas

Unary operators

Common unary overloads should be const when they don’t mutate, and return by value. For increment/decrement, prefer the canonical prefix/postfix forms:


// Example: a tiny counter type demonstrating unary overloads
#include <cstdint>

class Counter {
    std::int64_t n_ = 0;

public:
    Counter() = default;
    explicit Counter(std::int64_t n) : n_(n) {}

    // Unary plus/minus: do not modify; return a new value
    Counter operator+() const { return *this; }
    Counter operator-() const { return Counter{-n_}; }

    // Logical negation (example semantics): true when zero
    bool operator!() const noexcept { return n_ == 0; }

    // Bitwise not could be defined if semantics fit your domain
    // Counter operator~() const;

    // Prefix ++: modify then return by reference
    Counter& operator++() noexcept {
        ++n_;
        return *this;
    }

    // Postfix ++: dummy int parameter distinguishes it; return by value (old state)
    Counter operator++(int) noexcept {
        Counter old = *this;
        ++(*this);
        return old;
    }

    // Similarly for -- (prefix returns ref; postfix returns old-by-value)
    Counter& operator--() noexcept {
        --n_;
        return *this;
    }
    Counter operator--(int) noexcept {
        Counter old = *this;
        --(*this);
        return old;
    }

    std::int64_t value() const noexcept { return n_; }
};

Key points: Prefix ++x/--x returns by reference (expected chaining semantics). Postfix has a dummy int parameter and returns the old value by value; implement postfix in terms of prefix to avoid duplication.

Binary operators: member vs. non-member

When an operator modifies the left operand (e.g., +=), make it a member returning T&. For pure value-producing binary operators (e.g., +), prefer a non-member for symmetry; if access to internals is required, declare it as a hidden friend inside the class.


// 2D point: member compound ops, non-member arithmetic for symmetry
#include <ostream>

class Point {
    double x_ = 0, y_ = 0;

public:
    Point() = default;
    Point(double x, double y) : x_{x}, y_{y} {}

    // Member compound assignment: modifies *this, returns by reference
    Point& operator+=(const Point& rhs) noexcept {
        x_ += rhs.x_; y_ += rhs.y_;
        return *this;
    }
    Point& operator-=(const Point& rhs) noexcept {
        x_ -= rhs.x_; y_ -= rhs.y_;
        return *this;
    }

    // Hidden-friend non-members for symmetry (ADL finds them)
    friend Point operator+(Point lhs, const Point& rhs) noexcept {
        lhs += rhs;            // reuse compound op
        return lhs;            // return by value
    }
    friend Point operator-(Point lhs, const Point& rhs) noexcept {
        lhs -= rhs;
        return lhs;
    }

    // Streaming; hidden friend is common for access + ADL
    friend std::ostream& operator<<(std::ostream& os, const Point& p) {
        return os << '(' << p.x_ << ", " << p.y_ << ')';
    }
};

Why this structure? +=/-= are members because they mutate the left operand; +/- are non-members to keep both operands symmetric and to allow implicit conversions on the left-hand side.

Comparison operators (C++20)

Prefer writing one comparison and letting the compiler generate the rest:


#include <compare>

class Version {
    int major_ = 0, minor_ = 0;
public:
    Version() = default;
    Version(int maj, int min) : major_{maj}, minor_{min} {}

    // Defaulted three-way gives all six comparisons for free
    auto operator<=>(const Version&) const = default;
};

Design rules that prevent surprise

Unary increment/decrement: prefix vs. postfix at a glance


// Canonical signatures
T& operator++();    // prefix
T  operator++(int);  // postfix (int is a dummy)
T& operator--();    // prefix
T  operator--(int);  // postfix

What must be members?

The following operators can be overloaded only as nonstatic member functions: =, (), [], ->. (This is a language rule.)

A compact end-to-end example

Combine several practices: prefix/postfix, compound and derived binary, streaming.


#include <ostream>

class IntBox {
    int v_ = 0;
public:
    explicit IntBox(int v = 0) : v_{v} {}

    // Compound/mutating
    IntBox& operator+=(const IntBox& r) noexcept { v_ += r.v_; return *this; }
    IntBox& operator-=(const IntBox& r) noexcept { v_ -= r.v_; return *this; }

    // Derived value-producing (hidden friends)
    friend IntBox operator+(IntBox l, const IntBox& r) noexcept { l += r; return l; }
    friend IntBox operator-(IntBox l, const IntBox& r) noexcept { l -= r; return l; }

    // Unary prefix/postfix
    IntBox& operator++()    noexcept { ++v_; return *this; }  // prefix
    IntBox  operator++(int) noexcept { IntBox old=*this; ++(*this); return old; } // postfix

    // Stream it
    friend std::ostream& operator<<(std::ostream& os, const IntBox& b) {
        return os << b.v_;
    }
};

Common mistakes to avoid

Related lessons


SEMrush Software