Language[lang]

C++ operator overloading: signatures, member or friend

Section
Language [lang]
Standard
C++20
Compilers
MSVC 19.44, Clang 19.1 (MSVC STL), GCC 16.2, Clang 23.1 (libc++)
Published
Length
7 min read
In this answer9 clauses
  1. 1An overloaded operator is a function call
  2. 2Member or non-member, and the hidden friend
  3. 3The canonical signatures
  4. 4Comparisons since C++20: operator== and the spaceship operator
  5. 5How to overload operator<< and operator>>
  6. 6operator[], operator() and ++
  7. 7Conversion operators: make them explicit
  8. 8Operators you should not overload, and those you cannot
  9. 9Checklist for overloading operators

Abstract

Write + and the other symmetric arithmetic operators as non-member functions, so both operands convert alike, and the stream operator << as a non-member, because the stream is its left operand: hidden friends when they need private members. =, [], (), -> and conversion operators must be members. Build + from +=; return *this by reference from assignments and prefix ++, a new value from + and postfix ++. Since C++20, operator== and operator<=> declared = default as members give a type all six comparisons, because the compiler also tries them reversed.

Two hatched boxes with a purple plus sign between them; an arrow points down to one wide frame that starts with the same plus sign and holds both boxes side by side inside a pair of parentheses.
Figure 1 — An operator between two objects is a function call with two arguments.

C++ operator overloading means writing a function named operator+, operator== or operator<< with at least one operand of class or enumeration type; a + b then calls it. Write + and the other symmetric arithmetic operators as non-members, so both operands convert alike, and << as a non-member because the stream is its left operand: friends defined inside the class (hidden friends) when they need its private members. =, [], (), -> and conversion operators must be members. Since C++20, operator== and operator<=> can be defaulted members, because the compiler also tries them with the operands reversed, and the two give a type all six comparisons. Build + from +=; return *this by reference from assignments and prefix ++, a new value from + and postfix ++.

This answer is written for C++20, which added <=> and the rewriting of comparisons; C++23 added a few forms of [] and (), and C++26 nothing to overloading. The samples were built with MSVC 19.44, Clang 19.1 (MSVC STL), GCC 16.2 and Clang 23.1 (libc++). The first releases with <=> are in Since When’s C++20 table.

What changed: a type’s comparisons are written as six operator functions operator== and operator<=>, defaulted.

since C++20

An overloaded operator is a function call

a + b on a class type calls a function named operator+, which you can also call by name:

[Example 1:
struct Volts {
    double value;
};

Volts operator+(Volts a, Volts b) {
    return Volts{a.value + b.value};
}
// ...
Volts a{1.5};
Volts b{2.0};
Volts c = a + b;             // the compiler calls operator+(a, b)
Volts d = operator+(a, b);   // the same call, written out

— end example]

All four printed 3.5 3.5. At least one parameter must be of class or enumeration type, so operator+ on two doubles is an error, and you cannot invent operators or change their precedence or number of operands ([over.oper]).

Member or non-member, and the hidden friend

Make + and its kind non-members: a member operator’s left operand is the object itself, and the compiler converts only the right one. This Volts converts from double:

[Example 2:
class Volts {
public:
    Volts(double v) : value_{v} {}   // converts a double to Volts

    Volts operator+(const Volts& rhs) const {
        return Volts{value_ + rhs.value_};
    }
    double value() const { return value_; }

private:
    double value_;
};
// ...
Volts v{1.5};
Volts a = v + 2.0;   // fine: v.operator+(Volts{2.0})
Volts b = 2.0 + v;   // error: 2.0 is not made a Volts

— end example]

All four rejected the last line (GCC: no match for 'operator+' (operand types are 'double' and 'Volts')). A non-member treats both operands alike, which is why the Core Guidelines say to use non-member functions for symmetric operators. When it needs private members, define it inside the class as a friend:

[Example 3:
class Volts {
public:
    Volts(double v) : value_{v} {}   // converts a double to Volts
    double value() const { return value_; }

    friend Volts operator+(const Volts& a, const Volts& b) {
        return Volts{a.value_ + b.value_};   // private access
    }

private:
    double value_;
};

— end example]

Both v + 2.0 and 2.0 + v built and printed 3.5. A friend defined inside the class is a hidden friend: not a member, and found only through an operand of its class (argument-dependent lookup), so it is not considered when both operands are other types that merely convert to Volts.

== and <=> are the exception since C++20: the compiler also tries them with the operands reversed, so they can be members. The language requires =, [], (), -> and conversion operators to be members.

The canonical signatures

Volts stands for your value type; the samples built every signature here.

Operator Where Signature Returns
+= -= *= member Volts& operator+=(const Volts& rhs) *this
+ - * hidden friend friend Volts operator+(Volts lhs, const Volts& rhs) a new value
unary - hidden friend friend Volts operator-(const Volts& v) a new value
== member, defaulted bool operator==(const Volts&) const = default; bool
<=> member, defaulted auto operator<=>(const Volts&) const = default; a comparison category
<< non-member std::ostream& operator<<(std::ostream& os, const Volts& v) the stream
>> hidden friend friend std::istream& operator>>(std::istream& is, Volts& v) the stream
[] member, two overloads Volts& operator[](std::size_t i) and const Volts& operator[](std::size_t i) const a reference
() member Volts operator()(Volts v) const the result
prefix ++ member SensorId& operator++() *this
postfix ++ member SensorId operator++(int) the old value
unary *, -> member Reading& operator*() const and Reading* operator->() const a reference; a pointer
conversion member explicit operator bool() const the target type
= member Grid& operator=(const Grid& rhs) *this

+ takes its left operand by value, applies += to it and returns it, so the arithmetic is written once. Copy and move assignment are in the rule of zero. Give each operator only its conventional meaning: + adds, == compares.

Comparisons since C++20: operator== and the spaceship operator

Default operator== and operator<=>, the three-way comparison (or spaceship) operator, which compares two values in one call, and a type has all six comparisons:

[Example 4:
struct Volts {
    double value;
    bool operator==(const Volts&) const = default;
    auto operator<=>(const Volts&) const = default;

    bool operator==(double v) const { return value == v; }
};

struct Reading {
    int sensor;
    Volts volts;
    bool operator==(const Reading&) const = default;
    auto operator<=>(const Reading&) const = default;
};
// ...
Reading a{7, Volts{1.5}};
Reading b{7, Volts{2.0}};
std::cout << std::boolalpha
          << (a == b) << ' '    // false
          << (a != b) << ' '    // true:  !(a == b)
          << (a < b) << ' '     // true:  (a <=> b) < 0
          << (a <= b) << ' '    // true:  (a <=> b) <= 0
          << (a > b) << ' '     // false: (a <=> b) > 0
          << (a >= b) << '\n';  // false: (a <=> b) >= 0

Volts v{2.0};
std::cout << (v == 2.0) << ' '  // true
          << (2.0 == v) << '\n';  // true: v == 2.0, reversed

— end example]

All four printed the values in the comments. The compiler rewrites a != b as !(a == b) and the four orderings as a comparison of a <=> b with zero, and it also tries == and <=> with the operands reversed ([over.match.oper]). No operator== takes a double first, so 2.0 == v calls the member operator==(double) with the operands swapped, v.operator==(2.0):

Three rows: a != b becomes !(a == b); a less than b becomes the three-way comparison of a and b, less than zero; 2.0 == v becomes v == 2.0. A brace names operator== and the three-way comparison operator.
Figure 2 — Since C++20 the compiler rewrites the other comparisons, and the reversed forms, in terms of the two you define: operator== and the three-way comparison operator.

A defaulted <=> declares a defaulted == with it only when the class declares no operator== of its own ([class.compare.default]). This Volts declares operator==(double), so it needs its defaulted == line. A <=> written by hand declares no == at all: a < b then compiles and a == b fails (GCC: no match for 'operator==').

With auto, <=> returns a comparison category: std::strong_ordering for an int member, std::partial_ordering once a member is a double. What each promises to a sort is in how to write comparators for std::sort and std::map. On C++17, write all six by hand: != from ==, and >, <= and >= from <.

Mark comparison operators const: as C++20, an old member operator== that is not const is ambiguous with its own reversed form, an error on MSVC and a warning on GCC and Clang.

How to overload operator<< and operator>>

operator<< is a non-member, because the stream is the left operand, and it returns the stream so that calls chain:

[Example 5:
std::ostream& operator<<(std::ostream& os, const Volts& v) {
    return os << v.value() << " V";
}
// ...
Volts a{1.5};
Volts b{230.0};
std::cout << a << ", " << b << '\n';         // 1.5 V, 230 V

— end example]

operator>> mirrors it: a non-member that returns the stream, changes its argument only when the read worked, and leaves the stream failed on bad input:

[Example 6:
struct Volts {
    double value;

    friend std::istream& operator>>(std::istream& is, Volts& v) {
        double raw;
        if (is >> raw)      // a failed read sets the stream's failbit
            v.value = raw;  // v changes only when the read worked
        return is;
    }
};

— end example]

Reading 3.5 volts twice from a std::istringstream, all four printed read 3.5, then bad input, kept 3.5. For std::format and std::println, specialize std::formatter as well: std::println shows the smallest one for your own type.

operator[], operator() and ++

operator[] comes as the pair in the table: the non-const overload returns a reference a caller can write through, and the const one serves a const object. operator() makes a function object, an object called like a function (generic lambdas and function objects). Prefix ++ changes the object and returns it by reference; postfix ++ takes an unused int that only tells the two apart, and returns a copy of the old value:

[Example 7:
SensorId& operator++() {     // prefix: ++id
    ++n_;
    return *this;
}
SensorId operator++(int) {   // postfix: id++
    SensorId old = *this;
    ++*this;
    return old;
}
// ...
SensorId id{7};
SensorId next = ++id;   // id is 8, next is 8
SensorId old = id++;    // id is 9, old is 8

— end example]

All four printed 9 8 8. A prefix ++ that returns void breaks auto next = ++id;. C++23 adds operator[] with several indices (grid[1, 2]), static operator() and operator[] for objects with no state, and one operator[] for const and non-const objects alike through an explicit object parameter, this auto& self.

Conversion operators: make them explicit

An implicit conversion operator lets a type turn into another silently, and compiles code nobody meant:

[Example 8:
struct Volts {
    double value;
    operator double() const { return value; }   // implicit
};
// ...
Reading r{7, Volts{7.0}};
if (r.volts == r.sensor)   // compiles: volts against a sensor id
    std::cout << "7 V equals sensor 7\n";

— end example]

All four printed 7 V equals sensor 7. With explicit operator double(), that comparison is an error and static_cast<double>(r.volts) still converts. Given Reading an explicit operator bool(), if (r), !r and r && r.volts > 5.0 still compile, and int n = r; does not.

Operators you should not overload, and those you cannot

Four operators cannot be overloaded: ., .*, :: and ?:. Three can be and should not be: && and ||, which lose the built-in short-circuit, and the comma, which generic code expects to be the built-in one. An overloaded && is a function call, so both operands are evaluated:

[Example 9:
Verdict operator&&(Verdict a, Verdict b) {   // don't
    return Verdict{a.ok && b.ok};
}
// ...
Volts low{-1.0};   // out of range: the left operand is false
Volts high{3.3};

std::cout << "built-in &&\n";
bool a = in_range("left", low).ok && in_range("right", high).ok;

std::cout << "overloaded &&\n";
Verdict b = in_range("left", low) && in_range("right", high);

— end example]

in_range prints checked and the operand’s name. The program printed this; under overloaded && the two lines came in either order, depending on the compiler:

[Example 10:
built-in &&
  checked left
overloaded &&
  checked left
  checked right
false false

— end example]

Checklist for overloading operators

  1. + and its kind, << and >>: non-members, hidden friends when they need private members.
  2. =, [], (), ->, conversions, compound assignment and ++: members.
  3. == and <=>: defaulted, const members.
  4. Build + from +=. Return *this by reference from assignments and prefix ++, a value from + and postfix ++; clang-tidy’s misc-unconventional-assign-operator flags an operator= that does not.
  5. Make conversion operators explicit.
  6. Do not overload &&, || or the comma.

More answers about the core language are in Language.

Questions

Should an overloaded operator be a member or a friend function?

A member where the language requires one (=, [], (), -> and conversion operators) and for operators that modify the object, such as += and ++. Symmetric operators such as + are non-members, so both operands convert alike; one that needs private data is a hidden friend, a friend function defined inside the class. Since C++20, == and <=> can be defaulted members, because the compiler also tries them with the operands reversed; << is a non-member because the stream is its left operand.

How do I overload the << operator in C++?

Write a non-member function std::ostream& operator<<(std::ostream& os, const T& v) that writes the value to os and returns os, so calls chain. It is not a member of T, because a member operator takes the object as its left operand and here the stream is on the left. For std::format and std::print, specialize std::formatter for T as well.

How do I overload == in C++20?

Declare bool operator==(const T&) const = default; in the class: it compares the members in declaration order, and a != b works too, because C++20 rewrites it as !(a == b). For <, <=, > and >= add auto operator<=>(const T&) const = default;, which also declares a defaulted operator== as long as the class declares no operator== of its own.

What is the spaceship operator in C++?

It is <=>, the three-way comparison operator of C++20. Its result compares with zero: C++20 rewrites a < b as (a <=> b) < 0, so one defaulted operator<=> gives <, <=, > and >=. The result is a comparison category, such as std::strong_ordering for a type holding an int, or std::partial_ordering once a member is a double.

Which operators cannot be overloaded in C++?

Four: member access (.), pointer-to-member access (.*), scope resolution (::) and the conditional operator (?:). Three more can be overloaded and should not be. An overloaded && or || is a function call, so both operands are evaluated and the built-in short-circuit is lost; an overloaded comma is called by code that meant to sequence two expressions.

What is the difference between the copy constructor and the assignment operator?

The copy constructor makes a new object from an existing one; the copy assignment operator replaces the value of an object that already exists. Assignment must be a member, takes a const reference, returns *this by reference and must be safe for self-assignment. A class whose members copy themselves correctly needs neither written by hand: that is the rule of zero.

Read next

  1. Rule of zero: the special members C++ writes for youLanguage · Miriam Castellanos · written for C++20
  2. C++ move semantics: what std::move and && really doLanguage · Miriam Castellanos · written for C++20
  3. C++ enum and enum class: declare, convert, switchLanguage · Miriam Castellanos · written for C++11