C++ Operator Overloading
- Description: A note on the C++ operator overloading, function objects, and
std::function - My Notion Note ID: K2A-B1-9
- Created: 2018-09-15
- Updated: 2026-02-28
- License: Reuse is very welcome. Please credit Yu Zhang and link back to the original on yuzhang.io
Table of Contents
- 1. Operators as Function Calls
- 2. Member vs Nonmember: The Rules
- 3. Library-Defined Function Objects
- 4. Callable Objects and
std::function - 5. Three-Way Comparison (
<=>, C++20) - 6.
std::move_only_function(C++23)
1. Operators as Function Calls
- Operators = syntactic sugar for function calls.
- Member vs free function choice = key to idiomatic, correct overloads.
// These two are equivalent:
data1 + data2;
operator+(data1, data2); // Nonmember form
// These two are equivalent:
data1 += data2;
data1.operator+=(data2); // Member form (binds 'this' to data1)
2. Member vs Nonmember: The Rules
2.1 Must Be Member Functions
- Language requirement:
| Operator | Notes |
|---|---|
= (assignment) |
Should free existing resources, then create new ones. When taking an initializer_list, no self-assignment check is needed. |
[] (subscript) |
Should return a reference. Define both const and non-const versions. |
() (function call) |
Can have multiple overloads with different parameter lists. |
-> (member access) |
Should return a pointer or an object that also overloads ->. |
2.2 Should Be Member Functions
- Conventionally members:
| Operator | Notes |
|---|---|
+=, -=, etc. (compound assignment) |
Return a reference to *this (the left-hand operand). |
++, -- (increment/decrement) |
Define both prefix and postfix. Implement prefix first, then postfix in terms of prefix. |
- Prefix/postfix convention:
class Counter {
int value_ = 0;
public:
// Prefix: returns reference to modified object
Counter& operator++() {
++value_;
return *this;
}
// Postfix: takes a dummy int parameter, returns old value (by value, not reference)
Counter operator++(int) {
Counter old = *this;
++(*this); // Reuse prefix implementation
return old;
}
};
// Explicit postfix call syntax:
// counter.operator++(0);
2.3 Must Be Nonmember Functions
| Operator | Notes |
|---|---|
<<, >> (I/O) |
Usually declared as friend. The left operand is a stream, not your class. Ensure the data is left in a valid state on failure. |
2.4 Should Be Nonmember Functions
- Symmetric operators (neither operand privileged) → nonmember → allow implicit conversions on both sides:
| Operator | Notes |
|---|---|
+, -, *, / (arithmetic) |
Implement in terms of compound assignment (+=, etc.). |
==, != (equality) |
Implement one, define the other in terms of it. (In C++20, operator<=> can generate these automatically.) |
<, >, <=, >= (relational) |
Keep consistent with equality operators. |
&, |, ^ (bitwise) |
Follow the same pattern as arithmetic. |
3. Library-Defined Function Objects
<functional>— templated function objects for all standard operators.- Useful as comparators, in algorithms. Safe with pointers (unlike bare operators — unspecified behavior).
#include <functional>
#include <vector>
#include <algorithm>
std::vector<int> v = {3, 1, 4, 1, 5};
// Sort in descending order using std::greater
std::sort(v.begin(), v.end(), std::greater<int>());
// Sort pointers safely (bare < on pointers is technically unspecified)
std::vector<int*> ptrs = { /* ... */ };
std::sort(ptrs.begin(), ptrs.end(), std::less<int*>());
3 categories:
- Arithmetic —
plus,minus,multiplies,divides,modulus,negate - Comparison —
equal_to,not_equal_to,greater,greater_equal,less,less_equal - Logical —
logical_and,logical_or,logical_not
4. Callable Objects and std::function
4.1 Categories of Callables
- C++ callable types. Distinct types, but all invokable with
()and storable instd::function(see § 4.2).
-
Functions — regular definitions.
int add(int a, int b) { return a + b; } int r = add(2, 3); // 5 -
Function pointers — var holding fn address. Name decays to pointer in most contexts →
&addandaddboth work.int (*fn_ptr)(int, int) = &add; // or just `add` int r = fn_ptr(2, 3); // 5 -
Lambdas — anonymous function objects (C++11). Each has unique compiler-generated type.
auto multiply = [](int a, int b) { return a * b; }; int r = multiply(2, 3); // 6- Captures:
[x](int n) { return n + x; }(by value),[&x](by reference).
- Captures:
-
Bind expressions —
std::bindfixes some args, leaves rest as placeholders. Modern C++ → lambda preferred. See K2A-B1-20 §3.3.#include <functional> using namespace std::placeholders; auto add_5 = std::bind(add, _1, 5); // _1 = first call-site argument int r = add_5(10); // add(10, 5) = 15 -
Function objects (functors) — classes overloading
operator(). Can hold state across calls. Most flexible (compiler can inline through them, unlikestd::function).struct Counter { int count = 0; int operator()() { return ++count; } }; Counter c; c(); // 1 c(); // 2 c(); // 3
4.2 std::function
std::function— general-purpose polymorphic function wrapper. Stores any callable matching the call signature.- Solves: different callable types (fn pointers, lambdas, functors) are otherwise distinct types.
#include <functional>
#include <iostream>
int add(int a, int b) { return a + b; }
struct Multiply {
int operator()(int a, int b) const { return a * b; }
};
int main() {
// All of these have different types, but std::function unifies them
std::function<int(int, int)> fn;
fn = add;
std::cout << fn(2, 3) << std::endl; // 5
fn = Multiply{};
std::cout << fn(2, 3) << std::endl; // 6
fn = [](int a, int b) { return a - b; };
std::cout << fn(2, 3) << std::endl; // -1
return 0;
}
- Perf note: type erasure overhead + possible heap allocation. Hot paths → prefer templates or
autowith lambdas.
5. Three-Way Comparison (<=>, C++20)
- "Spaceship" operator
<=>returns an ordering — "less", "equal", "greater". - From single
<=>→ compiler synthesizes all 6 relational operators (<,<=,>,>=,==,!=).
#include <compare>
class Point {
public:
int x, y;
auto operator<=>(const Point&) const = default; // member-wise comparison
};
Point a{1, 2}, b{1, 3};
a < b; // true
a == b; // false
a != b; // true
= default→ compiler generates member-wise: comparexfirst, thenyif equal. All 6 free.
Ordering Categories
<=>return type selects ordering strength:
| Category | Strength | Meaning |
|---|---|---|
std::strong_ordering |
Total order with equality | a == b implies a and b are interchangeable |
std::weak_ordering |
Total order, equivalence | a == b may differ in non-salient ways (e.g., case-insensitive strings) |
std::partial_ordering |
Some pairs incomparable | Floats: NaN <=> NaN is unordered |
struct CaseInsensitive {
std::string value;
std::weak_ordering operator<=>(const CaseInsensitive& o) const {
return ci_compare(value, o.value); // returns weak_ordering
}
};
Custom <=>
- Hand-write
<=>for non-trivial comparisons. Compiler still synthesizes rest:
class Version {
int major_, minor_, patch_;
public:
auto operator<=>(const Version& o) const {
if (auto c = major_ <=> o.major_; c != 0) return c;
if (auto c = minor_ <=> o.minor_; c != 0) return c;
return patch_ <=> o.patch_;
}
bool operator==(const Version& o) const = default; // explicitly defaulted
};
- Note: defaulted
<=>implicitly declares defaultedoperator==(whyPointabove gets==free). User-provided (non-defaulted)<=>does not synthesize==— declare separately, often defaulted.
When to Use
- Default
auto operator<=>(const T&) const = default;— value types; all 6 free. - Custom
<=>— non-member-wise ordering (versions, case-insensitive strings, custom collations). - Hand-written
==/<— fine-grained control, or pre-C++20.
6. std::move_only_function (C++23)
std::move_only_function<Sig>— non-copyable callable wrapper. Likestd::functionbut holds move-only callables (lambdas capturingunique_ptr,std::packaged_task, etc.).
#include <functional>
#include <memory>
auto resource = std::make_unique<Resource>();
// std::function fails: it requires the callable to be CopyConstructible
// std::function<void()> bad = [r = std::move(resource)] { r->use(); }; // ERROR
// std::move_only_function works:
std::move_only_function<void()> task = [r = std::move(resource)] { r->use(); };
task();
When to prefer move_only_function
- Task queues — thread pools (each task consumed once).
- Callbacks capturing move-only state — futures, unique_ptrs, file handles.
- One-shot continuations — then-style chaining.
When to keep std::function
- Multi-cast — signal/slot dispatchers invoking same callable repeatedly.
- Storing in copyable containers —
std::vector<std::function<...>>shared by reference.
- Nearly identical syntax. Only difference = copyability constraint on stored callable.