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
- 1An overloaded operator is a function call
- 2Member or non-member, and the hidden friend
- 3The canonical signatures
- 4Comparisons since C++20: operator== and the spaceship operator
- 5How to overload operator<< and operator>>
- 6operator[], operator() and ++
- 7Conversion operators: make them explicit
- 8Operators you should not overload, and those you cannot
- 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.

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:
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:
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:
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:: |
the stream |
>> |
hidden friend | friend std:: |
the stream |
[] |
member, two overloads | Volts& operator[](std:: and const Volts& operator[](std:: |
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:
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):

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:: 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:
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:
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:
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:
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. 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:
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:
built-in &&
checked left
overloaded &&
checked left
checked right
false false— end example]
Checklist for overloading operators
+and its kind,<<and>>: non-members, hidden friends when they need private members.=,[],(),->, conversions, compound assignment and++: members.==and<=>: defaulted,constmembers.- Build
+from+=. Return*thisby reference from assignments and prefix++, a value from+and postfix++; clang-tidy’smisc-unconventional-assign-operatorflags anoperator=that does not. - Make conversion operators
explicit. - 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:: 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.
Sources
- [over.oper] Overloaded operatorsC++ standard working draft
- [over.match.oper] Operators in expressionsC++ standard working draft
- [class.compare.default] Defaulted comparison operator functionsC++ standard working draft
- [diff.cpp17.over] Compatibility with C++17: overloadingC++ standard working draft
- [hidden.friends] Hidden friendsC++ standard working draft
- operator overloadingcppreference.com
- C.161: Use non-member functions for symmetric operatorsC++ Core Guidelines