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

In C++, operators are syntactic sugar for function calls. Understanding which operators should be member functions vs free functions is key to writing 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

These operators must be defined as 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

These operators are conventionally defined as 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.

The standard 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 (where neither operand is privileged) should be nonmember functions to 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

The <functional> header provides templated function objects for all standard operators. These are useful as comparators, in algorithms, and are guaranteed to work correctly even with pointers (unlike bare operators).

#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*>());

The available function objects fall into three categories:

  1. Arithmetic: plus, minus, multiplies, divides, modulus, negate
  2. Comparison: equal_to, not_equal_to, greater, greater_equal, less, less_equal
  3. Logical: logical_and, logical_or, logical_not

4. Callable Objects and std::function

4.1 Categories of Callables

C++ has several types of callable objects. Each has a distinct type, but all of them can be invoked with () and stored in a std::function (see section 4.2).

  1. Functions — Regular function definitions.

    int add(int a, int b) { return a + b; }
    
    int r = add(2, 3);   // 5
    
  2. Function pointers — A variable holding the address of a function. The function's name decays to a pointer in most contexts, so &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 lambda has its own unique compiler-generated type.

    auto multiply = [](int a, int b) { return a * b; };
    
    int r = multiply(2, 3);           // 6
    

    Lambdas can capture surrounding variables — [x](int n) { return n + x; } for capture by value, [&x] for capture by reference.

  4. Bind expressions — Objects produced by std::bind, which fixes some of a callable's arguments and leaves the rest as placeholders. (Modern C++ usually prefers a lambda — 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 that overload operator(). Functors can hold state across calls and are the most flexible form (the compiler can also inline calls 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 is a general-purpose polymorphic function wrapper. It can store any callable object that matches the specified call signature, solving the problem that different callable types (function 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;
}

Performance note: std::function has overhead from type erasure and may allocate heap memory. For performance-critical code, prefer templates or auto with lambdas.


5. Three-Way Comparison (<=>, C++20)

The "spaceship operator" <=> returns an ordering — a value that says "less", "equal", or "greater". From a single <=>, the compiler can synthesize all six 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 makes the compiler generate a member-wise comparison: compare x first, then y if equal. All six operators come for free.

Ordering Categories

The return type of <=> selects the strength of ordering:

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 <=>

You can write <=> by hand for non-trivial comparisons. The compiler still synthesizes the rest of the relational operators:

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: A defaulted <=> implicitly declares a defaulted operator== (which is why the earlier Point example gets == for free). A user-provided (non-defaulted) <=> does not synthesize operator== — declare == separately, often by defaulting it as shown above.

When to Use

  1. Default to auto operator<=>(const T&) const = default; for value types — gives you all six operators for free.
  2. Custom <=> for ordering that isn't member-wise (versions, case-insensitive strings, custom collations).
  3. Stick with hand-written ==/< if you want fine-grained control or are pre-C++20.

6. std::move_only_function (C++23)

std::move_only_function<Sig> is a non-copyable callable wrapper — like std::function but able to hold move-only callables (lambdas with unique_ptr captures, 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 for thread pools (each task is consumed once).
  2. Callbacks that capture 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 where the same callable is invoked many times.
  2. Storing in copyable containers (e.g., std::vector<std::function<...>> shared by reference).

move_only_function and function have nearly identical syntax — the only difference is the copyability constraint on the stored callable.