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

  • 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:

  1. Arithmeticplus, minus, multiplies, divides, modulus, negate
  2. Comparisonequal_to, not_equal_to, greater, greater_equal, less, less_equal
  3. Logicallogical_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 in std::function (see § 4.2).
  1. Functions — regular definitions.

    int add(int a, int b) { return a + b; }
    
    int r = add(2, 3);   // 5
    
  2. Function pointers — var holding fn address. Name decays to pointer in most contexts → &add and add both work.

    int (*fn_ptr)(int, int) = &add;   // or just `add`
    
    int r = fn_ptr(2, 3);             // 5
    
  3. 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).
  4. Bind expressionsstd::bind fixes 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
    
  5. Function objects (functors) — classes overloading operator(). Can hold state across calls. Most flexible (compiler can inline through them, unlike std::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 auto with 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: compare x first, then y if 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 defaulted operator== (why Point above gets == free). User-provided (non-defaulted) <=> does not synthesize == — declare separately, often defaulted.

When to Use

  1. Default auto operator<=>(const T&) const = default; — value types; all 6 free.
  2. Custom <=> — non-member-wise ordering (versions, case-insensitive strings, custom collations).
  3. 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. Like std::function but holds move-only callables (lambdas capturing unique_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

  1. Task queues — thread pools (each task consumed once).
  2. Callbacks capturing move-only state — futures, unique_ptrs, file handles.
  3. One-shot continuations — then-style chaining.

When to keep std::function

  1. Multi-cast — signal/slot dispatchers invoking same callable repeatedly.
  2. Storing in copyable containersstd::vector<std::function<...>> shared by reference.
  • Nearly identical syntax. Only difference = copyability constraint on stored callable.