Operator Overloading  «Prev  Next»
Lesson 9 Overloading binary operators
Objective Write member functions in C++23 to overload binary operators

Overloading Binary Operators in C++23

Operator overloading lets your user-defined types behave like built-in types in expressions. In modern C++23, the goal is not “make everything overloadable.” The goal is to create small, readable, predictable types whose operators follow the same expectations programmers already have for +, -, *, ==, and friends.

This lesson shows how to overload binary operators using:

  • member functions (where the left operand is the implicit *this)
  • non-member functions (often declared friend when they need private access)

Binary operator mechanics: member vs non-member

A binary operator has two operands. When you overload it as a member function, the left operand becomes the implicit object (*this), and the right operand is the only explicit parameter:

  • Member overload: a + b becomes a.operator+(b)
  • Non-member overload: a + b becomes operator+(a, b)

That difference matters most for symmetrical operators, where you expect both operands to be treated the same (e.g., +, *, ==, !=). With a member overload, the left operand is “special” because it must already be your type (or implicitly convertible to it). With a non-member, both sides participate equally in overload resolution and conversions.

Guideline: prefer non-member overloads for symmetrical operators

For symmetrical binary operators, it is often normal and preferred to implement the operator as a non-member, frequently as a friend when private data is involved. This gives both operands the same rules for conversions and keeps your API more flexible.

In C++23, don’t be afraid of friend when it is narrowly used to implement a small set of operators. The key is to keep the friend surface area minimal and ensure the operator preserves class invariants.

Friend declarations: what they do (and what they do not do)

A friend declaration grants access to private members, but it does not automatically provide a visible declaration to users in all environments. The portable approach is:

  • Declare the friend inside the class (for access),
  • and also declare/define the function in the same header/source where users can see it.

Example: non-member (friend) operator+ for a Clock type

Continuing the Clock style example: implement operator+ as a non-member so both operands are treated symmetrically.




class Clock {
public:
    Clock() = default;
    explicit Clock(unsigned long seconds) : tot_secs(seconds) {}

    friend Clock operator+(const Clock& lhs, const Clock& rhs);

private:
    unsigned long tot_secs{0};
};

Clock operator+(const Clock& lhs, const Clock& rhs) {
    return Clock(lhs.tot_secs + rhs.tot_secs);
}
    

Modern improvements versus legacy style:

  • Use const Clock& parameters to avoid unnecessary copies. (Copying small types can be fine, but this is a clean default habit.)
  • Return a new Clock object to preserve value semantics. This is the usual expectation for arithmetic operators: they don’t mutate operands.
  • Keep the representation (tot_secs) private so operators cannot violate invariants accidentally.

Using a member function to overload a binary operator

A member overload is appropriate when the left operand must be your type and the operation is naturally “owned” by the class. In that case, remember the implicit first operand is *this.


class Clock {
public:
    Clock() = default;
    explicit Clock(unsigned long seconds) : tot_secs(seconds) {}

    Clock operator-(const Clock& rhs) const {
        return Clock(tot_secs - rhs.tot_secs);
    }

private:
    unsigned long tot_secs{0};
};
    

Note: - is also symmetrical in the “two operands” sense, but it is not commutative, and designs vary. The bigger issue is still conversions: if you want lhs and rhs treated identically for conversions, a non-member is often the safer choice.

Neighboring topics: operator contracts and modern best practices

  • Maintain invariants: if your type represents a constrained domain (e.g., normalized time), ensure operators return normalized results (or document behavior clearly).
  • Prefer compound assignment: if you overload +, consider also implementing operator+=. A common pattern is implement += as the core operation, then define + in terms of it.
  • Consistency matters: if == is overloaded, also overload !=, or rely on C++20+ defaulted comparisons where it fits your type design.
  • Avoid “clever” overloads: operators should feel obvious. If it reads like a trick, use a named function instead.

Overloading Binary Operators - Exercise

Click the Exercise link below to practice overloading several arithmetic binary operators in a class that implements a set.

Overloading Binary Operators - Exercise

SEMrush Software Target 9SEMrush Software Banner 9