Lesson 7 | Unary and binary operator overloading |
Objective | Understand correct signatures, when to prefer member or non-member, and how to avoid surprise. |
void main
, broad using namespace std;
), and missed the crucial difference between prefix and postfix signatures for ++
/--
.+=
, pre-increment) or on when to favor non-member overloads for symmetry.+
, -
, !
, ~
, ++
, --
).+
, -
, *
, /
, comparisons).=
, ()
, []
, and ->
must be overloaded as nonstatic member functions.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.
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.
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;
};
%
to mean “concatenate files” or ^
to mean exponent for matrices unless your audience expects it.T&
to support chaining; value-producing operators return by value.const
. Mark as noexcept
when appropriate.+=
and derive +
to avoid duplicate logic.rotate90()
), prefer it.
// Canonical signatures
T& operator++(); // prefix
T operator++(int); // postfix (int is a dummy)
T& operator--(); // prefix
T operator--(int); // postfix
The following operators can be overloaded only as nonstatic member functions: =
, ()
, []
, ->
. (This is a language rule.)
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_;
}
};
operator=
or operator+=
(should return T&
).int
parameter.