Note: The answers were given in a specific order, and have received varying amounts of votes over time. Since the order of answers depends on your answer sorting preferences, here's anindex of the answers in the order in which they make the most sense:
- The General Syntax of Operator Overloading in C++
- The Three Basic Rules of Operator Overloading in C++
- The Decision between Member and Non-member
- Common Operators to Overload
- Assignment Operator
- Stream Insertion and Extraction
- Function Call Operator
- Logical Operators
- Arithmetic Operators
- Subscript Operator
- Operators for Pointer-like Types
- Comparison Operators, Including C++20 Three-Way Comparison
- Conversion Operators
- Overloading new and delete
- Summary of Canonical Function Signatures
(Note: This is meant to be an entry toStack Overflow's C++ FAQ. If you want to critique the idea of providing an FAQ in this form, thenthe posting on meta that started all this would be the place to do that. Answers to that question are monitored in theC++ chatroom, where the FAQ idea started in the first place, so your answer is very likely to get read by those who came up with the idea.)
- 2Oh, and an English translation is also available:the basics andcommon practiceArne Mertz– Arne Mertz2017-07-31 14:13:18 +00:00CommentedJul 31, 2017 at 14:13
10 Answers10
Common Operators to Overload
Most of the work in overloading operators is boilerplate code. That is little wonder, since operators are merely syntactic sugar. Their actual work could be done by (and often is forwarded to) plain functions. But it is important that you get this boilerplate code right. If you fail, either your operator’s code won’t compile, your users’ code won’t compile, or your users’ code will behave surprisingly.
Assignment Operator
There's a lot to be said about assignment. However, most of it has already been said inGMan's famous Copy-And-Swap FAQ, so I'll skip most of it here, only listing the perfect assignment operator for reference:
X& X::operator=(X rhs){ swap(rhs); return *this;}Stream Insertion and Extraction
| Disclaimer |
|---|
For overloading<< and>> as bitwise shift operators, skip to the section Binary Arithmetic Operators. |
The bitwise shift operators<< and>>, although still used in hardware interfacing for the bit-manipulation functions they inherit from C, have become more prevalent as overloaded stream input and output operators in most applications.
The stream operators, among the most commonly overloaded operators, are binary infix operators for which the syntax does not specify any restriction on whether they should be members or non-members.However, their left operands are streams from the standard library, and you cannot add member functions to those1, so you need to implement these operators for your own types as non-member functions2.The canonical forms of the two are these:
std::ostream& operator<<(std::ostream& os, const T& obj){ // Write obj to stream return os;}std::istream& operator>>(std::istream& is, T& obj){ // Read obj from stream if( /* no valid object of T found in stream */ ) is.setstate(std::ios::failbit); return is;}When implementingoperator>>, manually setting the stream’s state is only necessary when the reading itself succeeded, but the result is not what would be expected.
1Note that some of the<< overloads of the standard library are implemented as member functions, and some as free functions. Only the locale-dependent functions are member functions, such asoperator<<(long).
2According to the rules of thumb, the insertion/extraction operators should be member functions because they modify the left operand. However, we cannot follow the rules of thumb here.
Function Call Operator
The function call operator, used to create function objects, also known asfunctors, must be defined as amember function, so it always has the implicitthis argument of member functions. Other than this, it can be overloaded to take any number of additional arguments, including zero.
Here's an example of the syntax:
struct X { // Overloaded call operator int operator()(const std::string& y) { return /* ... */; }};Usage:
X f;int a = f("hello");Throughout the C++ standard library, function objects are always copied. Your own function objects should therefore be cheap to copy. If a function object absolutely needs to use data which is expensive to copy, it is better to store that data elsewhere and have the function object refer to it.
Comparison Operators
| This section has been moved elsewhere |
|---|
Seethis FAQ answer for overloading the binary infix==,!=,<,>,<=, and>= operators, as well as the<=> three-way comparison, aka. "spaceship operator" in C++20. There is so much to say about comparison operators that it would exceed the scope of this answer. |
In the most simple case, you can overload all comparison comparison operators by defaulting<=> inC++20:
#include <compare>struct X { // defines ==, !=, <, >, <=, >=, <=> friend auto operator<=>(const X&, const X&) = default;};If you can't do this, continue to the linked answer.
Logical Operators
The unary prefix negation! should be implemented as a member function. It is usually not a good idea to overload it because of how rare and surprising it is.
struct X { X operator!() const { return /* ... */; }};The remaining binary logical operators (||,&&) should be implemented as free functions. However, it isvery unlikely that you would find a reasonable use case for these1.
X operator&&(const X& lhs, const X& rhs) { return /* ... */; }X operator||(const X& lhs, const X& rhs) { return /* ... */; }1It should be noted that the built-in version of|| and&& use shortcut semantics. While the user defined ones (because they are syntactic sugar for method calls) do not use shortcut semantics. User will expect these operators to have shortcut semantics, and their code may depend on it, Therefore it is highly advised NEVER to define them.
Arithmetic Operators
Unary Arithmetic Operators
The unary increment and decrement operators come in both prefix and postfix flavor. To tell one from the other, the postfix variants take an additional dummy int argument. If you overload increment or decrement, be sure to always implement both prefix and postfix versions.
Here is the canonical implementation of increment, decrement follows the same rules:
struct X { X& operator++() { // Do actual increment return *this; } X operator++(int) { X tmp(*this); operator++(); return tmp; }};Note that the postfix variant is implemented in terms of prefix. Also note that postfix does an extra copy.1
Overloading unary minus and plus is not very common and probably best avoided. If needed, they should probably be overloaded as member functions.
1Also note that the postfix variant does more work and is therefore less efficient to use than the prefix variant. This is a good reason to generally prefer prefix increment over postfix increment. While compilers can usually optimize away the additional work of postfix increment for built-in types, they might not be able to do the same for user-defined types (which could be something as innocently looking as a list iterator). Once you got used to doi++, it becomes very hard to remember to do++i instead wheni is not of a built-in type (plus you'd have to change code when changing a type), so it is better to make a habit of always using prefix increment, unless postfix is explicitly needed.
Binary Arithmetic Operators
For the binary arithmetic operators, do not forget to obey the third basic rule operator overloading: If you provide+, also provide+=, if you provide-, do not omit-=, etc.Andrew Koenig is said to have been the first to observe that the compound assignment operators can be used as a base for their non-compound counterparts. That is, operator+ is implemented in terms of+=,- is implemented in terms of-=, etc.
According to our rules of thumb,+ and its companions should be non-members, while their compound assignment counterparts (+=, etc.), changing their left argument, should be a member. Here is the exemplary code for+= and+; the other binary arithmetic operators should be implemented in the same way:
struct X { X& operator+=(const X& rhs) { // actual addition of rhs to *this return *this; }};inline X operator+(const X& lhs, const X& rhs){ X result = lhs; result += rhs; return result;}operator+= returns its result per reference, whileoperator+ returns a copy of its result. Of course, returning a reference is usually more efficient than returning a copy, but in the case ofoperator+, there is no way around the copying. When you writea + b, you expect the result to be a new value, which is whyoperator+ has to return a new value.1
Also note thatoperator+ can be slightly shortened by passinglhs by value, not by reference.However, this would be leaking implementation details, make the function signature asymmetric, and would preventnamed return value optimization whereresult is the same object as the one being returned.
Sometimes, it's impractical to implement@ in terms of@=, such as for matrix multiplication.In that case, you can also delegate@= to@:
struct Matrix { // You can also define non-member functions inside the class, i.e. "hidden friends" friend Matrix operator*(const Matrix& lhs, const Matrix& rhs) { Matrix result; // Do matrix multiplication return result; } Matrix& operator*=(const Matrix& rhs) { return *this = *this * rhs; // Assuming operator= returns a reference }};The bit manipulation operators~&|^<<>> should be implemented in the same way as the arithmetic operators. However, (except for overloading<< and>> for output and input) there are very few reasonable use cases for overloading these.
1Again, the lesson to be taken from this is thata += b is, in general, more efficient thana + b and should be preferred if possible.
Subscript Operator
The subscript operator is a binary operator which must be implemented as a class member. It is used for container-like types that allow access to their data elements by a key.The canonical form of providing these is this:
struct X { value_type& operator[](index_type idx); const value_type& operator[](index_type idx) const; // ...};Unless you do not want users of your class to be able to change data elements returned byoperator[] (in which case you can omit the non-const variant), you should always provide both variants of the operator.
Operators for Pointer-like Types
For defining your own iterators or smart pointers, you have to overload the unary prefix dereference operator* and the binary infix pointer member access operator->:
struct my_ptr { value_type& operator*(); const value_type& operator*() const; value_type* operator->(); const value_type* operator->() const;};Note that these, too, will almost always need both a const and a non-const version.For the-> operator, ifvalue_type is ofclass (orstruct orunion) type, anotheroperator->() is called recursively, until anoperator->() returns a value of non-class type.
The unary address-of operator should never be overloaded.
Foroperator->*() (and more details aboutoperator->) seethis question.operator->*() is rarely used and thus rarely ever overloaded. In fact, even iterators do not overload it.
Continue toConversion Operators.
23 Comments
operator->() is actuallyextremely weird. It's not required to return avalue_type* -- in fact, it can return another class type,provided that class type has anoperator->(), which will then be called subsequently. This recursive calling ofoperator->()s proceeds until avalue_type* return type occurs. Madness! :)operatorX in terms ofoperatorX= if not practical. For matrices and polynomials (english?), code factorization of the multiplication goes the other way:operator*= should be defined in terms ofoperator*.* in terms of*= but it would be awkward because one of the first operation of*= would to create a new object, result of the computation. Then, after the for-ijk loop, we would swap this temporary object with*this. ie. 1.copy, 2.operator*, 3.swapT* const returning aconst T& on dereferencing, which is not the case. Or in other words: a const pointer does not imply a const pointee. In fact, it is not trivial to mimicT const * - which is the reason for the wholeconst_iterator stuff in the standard library. Conclusion: the signature should bereference_type operator*() const; pointer_type operator->() constThe Three Basic Rules of Operator Overloading in C++
When it comes to operator overloading in C++, there arethree basic rules you should follow. As with all such rules, there are indeed exceptions. Sometimes people have deviated from them and the outcome was not bad code, but such positive deviations are few and far between. At the very least, 99 out of 100 such deviations I have seen were unjustified. However, it might just as well have been 999 out of 1000. So you’d better stick to the following rules.
Whenever the meaning of an operator is not obviously clear and undisputed, it should not be overloaded.Instead, provide a function with a well-chosen name.
Basically, the first and foremost rule for overloading operators, at its very heart, says:Don’t do it. That might seem strange, because there is a lot to be known about operator overloading and so a lot of articles, book chapters, and other texts deal with all this. But despite this seemingly obvious evidence,there are only a surprisingly few cases where operator overloading is appropriate. The reason is that actually it is hard to understand the semantics behind the application of an operator unless the use of the operator in the application domain is well known and undisputed. Contrary to popular belief, this is hardly ever the case.Always stick to the operator’s well-known semantics.
C++ poses no limitations on the semantics of overloaded operators. Your compiler will happily accept code that implements the binary+operator to subtract from its right operand. However, the users of such an operator would never suspect the expressiona + bto subtractafromb. Of course, this supposes that the semantics of the operator in the application domain is undisputed.Always provide all out of a set of related operations.
Operators are related to each other and to other operations. If your type supportsa + b, users will expect to be able to calla += b, too. If it supports prefix increment++a, they will expecta++to work as well. If they can check whethera < b, they will most certainly expect to also to be able to check whethera > b. If they can copy-construct your type, they expect assignment to work as well.
Continue toThe Decision between Member and Non-member.
44 Comments
boost::spirit lol.+ for string concatenation is a violation, but it has by now become well established praxis, so that it seems natural. Although I do remember a home-brew string class I saw in the 90ies that used binary& for this purpose (referring to BASIC for established praxis). But, yeah, putting it into the std lib basically set this in stone. The same goes for abusing<< and>> for IO, BTW. Why would left-shifting be the obvious output operation? Because we all learned about it when we saw our first "Hello, world!" application. And for no other reason.operator== is that it should be an equivalence relation (IOW, you should not use non signaling NaN). There are many useful equivalence relations on containers. What does equality means? "a equalsb" means thata andb have the same mathematical value. The concept of mathematical value of a (non-NaN)float is clear, but the mathematical value of a container can have many distinct (type recursive) useful definitions. The strongest definition of equality is "they are the same objects", and it is useless.The Decision between Member and Non-member
| Category | Operators | Decision |
|---|---|---|
| Mandatory member functions | [],(),=,->, ... | member function (mandated by the C++ standard) |
| Pointer-to-member access | ->* | member function |
| Unary | ++,-,*,new, ... | member function, except for enumerations |
| Compound assignment | +=,|=,*=, ... | member function, except for enumerations |
| Other operators | +,==,<=>,/, ... | prefer non-member |
The binary operators= (assignment),[] (array subscription),-> (member access), as well as the n-ary() (function call) operator, must always be implemented asmember functions, because the syntax of the language requires them to.
Other operators can be implemented either as members or as non-members. Some of them, however, usually have to be implemented as non-member functions, because their left operand cannot be modified by you. The most prominent of these are the input and output operators<< and>>, whose left operands are stream classes from the standard library which you cannot change.
For all operators where you have to choose to either implement them as a member function or a non-member function,use the following rules of thumb to decide:
- If it is aunary operator, implement it as amember function.
- If a binary operator treatsboth operands equally (it leaves them unchanged), implement this operator as anon-member function.
- If a binary operator doesnot treat both of its operandsequally (usually it will change its left operand), it might be useful to make it amember function of its left operand’s type, if it has to access the operand's private parts.
Of course, as with all rules of thumb, there are exceptions. If you have a type
enum Month {Jan, Feb, ..., Nov, Dec}and you want to overload the increment and decrement operators for it, you cannot do this as a member functions, since in C++, enum types cannot have member functions. So you have to overload it as a free function. Andoperator<() for a class template nested within a class template is much easier to write and read when done as a member function inline in the class definition. But these are indeed rare exceptions.
(However,if you make an exception, do not forget the issue ofconst-ness for the operand that, for member functions, becomes the implicitthis argument. If the operator as a non-member function would take its left-most argument as aconst reference, the same operator as a member function needs to have aconst at the end to make*this aconst reference.)
Continue toCommon operators to overload.
34 Comments
operator+=() not being a member. It has to change its left-hand operand, so by definition it has to dig deep into its innards. What would you gain by not making it a member?operator += and theappend methods. Theappend method is more complete, because you can append a substring of the parameter from index i to index n -1:append(string, start, end) It seems logical to have+= call append withstart = 0 andend = string.size. At that moment, append could be a member method, butoperator += doesn't need to be a member, and making it a non-member would decrease the quantity of code playing with the String innards, so it is a good thing.... ^_^ ...The General Syntax of operator overloading in C++
You cannot change the meaning of operators for built-in types in C++, operators can only be overloaded for user-defined types1. That is, at least one of the operands has to be of a user-defined type. As with other overloaded functions, operators can be overloaded for a certain set of parameters only once.
Not all operators can be overloaded in C++. Among the operators that cannot be overloaded are:.::sizeoftypeid.* and the only ternary operator in C++,?:
Among the operators that can be overloaded in C++ are these:
| Category | Operators | Arity and Placement |
|---|---|---|
| Arithmetic | +-*/% and+=-=*=/=%= | binary infix |
+- | unary prefix | |
++-- | unary prefix and postfix | |
| Bitwise | &|^<<>> and&=|=^=<<=>>= | binary infix |
~ | unary prefix | |
| Comparison | ==!=<><=>=<=> | binary infix |
| Logical | ||&& | binary infix |
! | unary prefix | |
| Allocation functions | newnew[]deletedelete[] | unary prefix |
| User-defined conversions | T | unary |
| Assignment | = | binary infix |
| Member access | ->->* | binary infix |
| Indirection/Address-of | *& | unary prefix |
| Function call | () | N-ary postfix |
| Subscript | [] | N-ary2 postfix |
| Coroutine await | co_await | unary prefix |
| Comma | , | binary infix |
However, the fact that youcan overload all of these does not mean youshould do so. Seethe next answer.
In C++, operators are overloaded in the form offunctions with special names. As with other functions, overloaded operators can generally be implemented either as amember function of their left operand's type or asnon-member functions. Whether you are free to choose or bound to use either one depends on several criteria.3 A unary operator@4, applied to an object x, is invoked either asoperator@(x) or asx.operator@(). A binary infix operator@, applied to the objectsx andy, is called either asoperator@(x,y) or asx.operator@(y).5
Operators that are implemented as non-member functions are sometimes friend of their operand’s type.
1The term “user-defined” might be slightly misleading. C++ makes the distinction between built-in types and user-defined types. To the former belong for example int, char, and double; to the latter belong all struct, class, union, and enum types, including those from the standard library, even though they are not, as such, defined by users.
2The subscript operator used to be binary, not N-ary until C++23.
3This is covered ina later part of this FAQ.
4The@ is not a valid operator in C++ which is why I use it as a placeholder.
5The only ternary operator in C++ cannot be overloaded and the only n-ary operator must always be implemented as a member function.
Continue toThe Three Basic Rules of Operator Overloading in C++.
1 Comment
Conversion Operators (also known as User Defined Conversions)
In C++ you can create conversion operators, operators that allow the compiler to convert between your types and other defined types. There are two types of conversion operators, implicit and explicit ones.
Implicit Conversion Operators (C++98/C++03 and C++11)
An implicit conversion operator allows the compiler to implicitly convert (like the conversion betweenint andlong) the value of a user-defined type to some other type.
The following is a simple class with an implicit conversion operator:
class my_string {public: operator const char*() const {return data_;} // This is the conversion operatorprivate: const char* data_;};Implicit conversion operators, like one-argument constructors, are user-defined conversions. Compilers will grant one user-defined conversion when trying to match a call to an overloaded function.
void f(const char*);my_string str;f(str); // same as f( str.operator const char*() )At first this seems very helpful, but the problem with this is that the implicit conversion even kicks in when it isn’t expected to. In the following code,void f(const char*) will be called becausemy_string() is not anlvalue, so the first does not match:
void f(my_string&);void f(const char*);f(my_string());Beginners easily get this wrong and even experienced C++ programmers are sometimes surprised because the compiler picks an overload they didn’t suspect. These problems can be mitigated by explicit conversion operators.
Explicit Conversion Operators (C++11)
Unlike implicit conversion operators, explicit conversion operators will never kick in when you don't expect them to. The following is a simple class with an explicit conversion operator:
class my_string {public: explicit operator const char*() const {return data_;}private: const char* data_;};Notice theexplicit. Now when you try to execute the unexpected code from the implicit conversion operators, you get a compiler error:
prog.cpp: In function ‘int main()’:prog.cpp:15:18: error: no matching function for call to ‘f(my_string)’prog.cpp:15:18: note: candidates are:prog.cpp:11:10: note: void f(my_string&)prog.cpp:11:10: note: no known conversion for argument 1 from ‘my_string’ to ‘my_string&’prog.cpp:12:10: note: void f(const char*)prog.cpp:12:10: note: no known conversion for argument 1 from ‘my_string’ to ‘const char*’
To invoke the explicit cast operator, you have to usestatic_cast, a C-style cast, or a constructor style cast ( i.e.T(value) ).
However, there is one exception to this: The compiler is allowed to implicitly convert tobool. In addition, the compiler is not allowed to do another implicit conversion after it converts tobool (a compiler is allowed to do 2 implicit conversions at a time, but only 1 user-defined conversion at max).
Because the compiler will not cast "past"bool, explicit conversion operators now remove the need for theSafe Bool idiom. For example, smart pointers before C++11 used the Safe Bool idiom to prevent conversions to integral types. In C++11, the smart pointers use an explicit operator instead because the compiler is not allowed to implicitly convert to an integral type after it explicitly converted a type to bool.
Continue toOverloadingnew anddelete.
Comments
Overloadingnew anddelete operators
Note: This only deals with thesyntax of overloadingnew anddelete, not with theimplementation of such overloaded operators. I think that the semantics of overloadingnew anddelete deserve their own FAQ, within the topic of operator overloading I can never do it justice.
Basics
In C++, when you write anew expression likenew T(arg) two things happen when this expression is evaluated: Firstoperator new is invoked to obtain raw memory, and then the appropriate constructor ofT is invoked to turn this raw memory into a valid object. Likewise, when you delete an object, first its destructor is called, and then the memory is returned tooperator delete.
C++ allows you to tune both of these operations: memory management and the construction/destruction of the object at the allocated memory. The latter is done by writing constructors and destructors for a class. Fine-tuning memory management is done by writing your ownoperator new andoperator delete.
The first of the basic rules of operator overloading –don’t do it – applies especially to overloadingnew anddelete. Almost the only reasons to overload these operators areperformance problems andmemory constraints, and in many cases, other actions, likechanges to the algorithms used, will provide a muchhigher cost/gain ratio than attempting to tweak memory management.
The C++ standard library comes with a set of predefinednew anddelete operators. The most important ones are these:
void* operator new(std::size_t) throw(std::bad_alloc); void operator delete(void*) throw(); void* operator new[](std::size_t) throw(std::bad_alloc); void operator delete[](void*) throw();The first two allocate/deallocate memory for an object, the latter two for an array of objects. If you provide your own versions of these, they willnot overload, but replace the ones from the standard library.
If you overloadoperator new, you should always also overload the matchingoperator delete, even if you never intend to call it. The reason is that, if a constructor throws during the evaluation of a new expression, the run-time system will return the memory to theoperator delete matching theoperator new that was called to allocate the memory to create the object in. If you do not provide a matchingoperator delete, the default one is called, which is almost always wrong.
If you overloadnew anddelete, you should consider overloading the array variants, too.
Placementnew
C++ allows new and delete operators to take additional arguments.
So-called placement new allows you to create an object at a certain address which is passed to:
class X { /* ... */ };char buffer[ sizeof(X) ];void f(){ X* p = new(buffer) X(/*...*/); // ... p->~X(); // call destructor }The standard library comes with the appropriate overloads of the new and delete operators for this:
void* operator new(std::size_t,void* p) throw(std::bad_alloc); void operator delete(void* p,void*) throw(); void* operator new[](std::size_t,void* p) throw(std::bad_alloc); void operator delete[](void* p,void*) throw();Note that, in the example code for placement new given above,operator delete is never called, unless the constructor of X throws an exception.
You can also overloadnew anddelete with other arguments. As with the additional argument for placement new, these arguments are also listed within parentheses after the keywordnew. Merely for historical reasons, such variants are often also called placement new, even if their arguments are not for placing an object at a specific address.
Class-specific new and delete
Most commonly you will want to fine-tune memory management because measurement has shown that instances of a specific class, or of a group of related classes, are created and destroyed often and that the default memory management of the run-time system, tuned for general performance, deals inefficiently in this specific case. To improve this, you can overload new and delete for a specific class:
class my_class { public: // ... void* operator new(std::size_t); void operator delete(void*); void* operator new[](std::size_t); void operator delete[](void*); // ... };Overloaded thus, new and delete behave like static member functions. For objects ofmy_class, thestd::size_t argument will always besizeof(my_class). However, these operators are also called for dynamically allocated objects ofderived classes, in which case it might be greater than that.
Global new and delete
To overload the global new and delete, simply replace the pre-defined operators of the standard library with our own. However, this rarely ever needs to be done.
6 Comments
nothrow new ? The rule of thumb is that whenever you overload new, you must write 12 functions:[array] [{ placement | nothrow }] { new | delete }.nothrow new is more or less just for legacy stuff, I haven't ever even mentioned it. What would you suggest should be written about it here?nothrow new.new(buffer) X(/*...*/) what actually guarantees thatbuffer is properly aligned forX?Why can'toperator<< function for streaming objects tostd::cout or to a file be a member function?
Let's say you have:
struct Foo{ int a; double b; std::ostream& operator<<(std::ostream& out) const { return out << a << " " << b; }};Given that, you cannot use:
Foo f = {10, 20.0};std::cout << f;Sinceoperator<< is overloaded as a member function ofFoo, the LHS of the operator must be aFoo object. Which means, you will be required to use:
Foo f = {10, 20.0};f << std::coutwhich is very non-intuitive.
If you define it as a non-member function,
struct Foo{ int a; double b;};std::ostream& operator<<(std::ostream& out, Foo const& f){ return out << f.a << " " << f.b;}You will be able to use:
Foo f = {10, 20.0};std::cout << f;which is very intuitive.
2 Comments
<< to be used withstd::cout but what about overloading<< to be used with the same class. In that case it can be member function right?Comparison Operators, including Three-Way Comparison(C++20)
There areequality comparisons== and!=,andrelational comparisons<,>,<=,>=.C++20 has also introduced thethree-way comparison operator<=>.
| Operator | Meaning and Notes (Old) | Meaning and Notes (C++20) |
|---|---|---|
x == y | true ifx andy are equalsatisfiesEqualityComparable (used by std::unordered_map) | true ifx andy are equal(can be implemented as (x <=> y) == 0,but usually isn't) satisfies std::equality_comparable |
x != y | !(x == y) | !(x == y) |
x < y | true ifx is lower thanysatisfiesLessThanComparable (used by std::set,std::sort, etc.but requiresstrict weak ordering) | (x <=> y) < 0may satisfy std::strict_weak_orderingwhen wrapped in a functor (e.g. std::ranges::less) |
x > y | y < x | (x <=> y) > 0 |
x <= y | !(x < y) for strong orderings,x == y || x < y otherwise | (x <=> y) <= 0 |
x >= y | y <= x | (x <=> y) >= 0 |
x <=> y | N/A | three-way comparison aka. "spaceship operator" satisfies std::three_way_comparable |
Guidelines
- Comparison operators shouldn't be member functions.1)
- If you define
==, define!=too (unless it is rewritten in C++20). - If you define
<, define>,<=, and>=too. - (C++20) Prefer defining
<=>over defining each relational operator. - (C++20) Prefer defaulting operators over implementing manually.
- Equality and relational comparisons should match, meaning that
x == yshould be equivalent to!(x < y) && !(y < x)2) - Don't define
==in terms of<, even when you could3)
1)Otherwise, implicit conversions would be asymmetrical, and== is expected to apply the same kinds of implicit conversions to both sides.
2)This equivalence does not apply tofloat, but does apply toint and other strongly ordered types.
3)This is motivated by readability, correctness, and performance.
Implementation and Common Idioms Prior to C++20
| Disclaimer |
|---|
| If you're using C++20, the implementations in this section have been obsoleted. Skip ahead to the C++20 parts unless you're interested in a historical perspective. |
All operators are typically implemented as non-member functions, possibly ashidden friends (friends where the function is defined inside the class).All following code examples use hidden friends because this becomes necessary if you need to compare private members anyway.
struct S { int x, y, z; // (In)equality comparison: // implementing a member-wise equality friend bool operator==(const S& l, const S& r) { return l.x == r.x && l.y == r.y && l.z == r.z; } friend bool operator!=(const S& l, const S& r) { return !(l == r); } // Relational comparisons: // implementing a lexicographical comparison which induces a // strict weak ordering. friend bool operator<(const S& l, const S& r) { if (l.x < r.x) return true; // notice how all sub-comparisons if (r.x < l.x) return false; // are implemented in terms of < if (l.y < r.y) return true; if (r.y < l.y) return false; // also see below for a possibly simpler return l.z < r.z; // implementation } friend bool operator>(const S& l, const S& r) { return r < l; } friend bool operator<=(const S& l, const S& r) { return !(r < l); } friend bool operator>=(const S& l, const S& r) { return !(l < r); }};Note: in C++11, all of these can typically benoexcept andconstexpr.
Implementing all relational comparisons in terms of< is not valid if we have a partially ordered member (e.g.float).In that case,<= and>= must be written differently.
friend bool operator<=(const S& l, const S& r) { return l == r || l < r; }friend bool operator>=(const S& l, const S& r) { return r <= l; }Further Notes onoperator<
The implementation ofoperator< is not so simple because a proper lexicographical comparison cannot simply compare each member once.{1, 2} < {3, 0} should be true, even though2 < 0 is false.
A lexicographical comparison is a simple way of implementing astrict weak ordering, which is needed for containers likestd::set and algorithms likestd::sort. In short, astrict weak ordering should behave like the< operator for integers, except that some integers are allowed to be equivalent (e.g. for all even integers,x < y is false).
Ifx != y is equivalent tox < y || y < x, a simpler approach is possible:
friend bool operator<(const S& l, const S& r) { if (l.x != r.x) return l.x < r.x; if (l.y != r.y) return l.y < r.y; return l.z < r.z;}Common Idioms
For multiple members, you can usestd::tie to implement comparison lexicographically:
#include <tuple>struct S { int x, y, z; friend bool operator<(const S& l, const S& r) { return std::tie(l.x, l.y, l.z) < std::tie(r.x, r.y, r.z); }};Usestd::lexicographical_compare for array members.
Some people use macros or the curiously recurring template pattern (CRTP) to save the boilerplate of delegating!=,>,>=, and<=, or to imitate C++20's three-way comparison.
It is also possible to usestd::rel_ops (deprecated in C++20) to delegate!=,>,<=, and>= to< and== for all types in some scope.
Default Comparisons (C++20)
A substantial amount of comparison operators simply compare each member of a class.If so, the implementation is pure boilerplate and we can let the compiler do it all:
struct S { int x, y, z; // ==, !=, <, >, <=, >= are all defined. // constexpr and noexcept are inferred automatically. friend auto operator<=>(const S&, const S&) = default;};Note: defaulted comparison operators need to befriends of the class, and the easiest way to accomplish that is by defining them as defaulted inside the class. This makes them "hidden friends".
Alternatively, we can default individual comparison operators.This is useful if we want to define equality comparison, or only relational comparison:
friend bool operator==(const S&, const S&) = default; // inside SSee thecppreference article on default comparison.
Expression Rewriting (C++20)
In C++20, if a comparison operator isn't user-declared, the compiler will also try to userewrite candidates.Thanks to this, even if<=> isn't defaulted (which would implementall operators), we only have to implement== and<=>, and all other comparisons are rewritten in terms of these two.
| Operator | Potential Rewrites |
|---|---|
x == y | y == x |
x != y | !(x == y) or!(y == x) if equality comparison returnsbool |
x < y | (x <=> y) < 0 or0 < (y <=> x) if comparison result is comparable to zero |
x > y | (x <=> y) > 0 or0 > (y <=> x) if ... |
x <= y | (x <=> y) <= 0 or0 <= (y <=> x) if ... |
x >= y | (x <=> y) >= 0 or0 >= (y <=> x) if ... |
struct S { int x, y, z; // ==, != friend constexpr bool operator==(const S& l, const S& r) noexcept { /* ... */ } // <=>, <, >, <=, >= friend constexpr auto operator<=>(const S& l, const S& r) noexcept { /* ... */ }};Note:constexpr andnoexcept are optional, but can almost always be applied to comparison operators.
Note: BeforeP1185R2:<=> != ==, ax == y expression would also be rewritten as(x <=> y) == 0, but this would have been inefficient because it misses short-circuiting opportunities.
Three-Way Comparison Operator (C++20)
Note: it is colloquially called "spaceship operator". See alsospaceship-operator.
The basic idea behindx <=> y is that the result tells us whetherx is lower than, greater than, equivalent to, or unordered withy.This is similar to functions likestrcmp in C.
// old C styleint compare(int x, int y) { if (x < y) return -1; if (x > y) return 1; return 0; // or simply return (x > y) - (x < y);}// C++20 style: this is what <=> does for int.auto compare_cxx20(int x, int y) { if (x < y) return std::strong_ordering::less; if (x > y) return std::strong_ordering::greater; return std::strong_ordering::equal;}// This is what <=> does for float.auto compare_cxx20(float x, float y) { if (x < y) return std::partial_ordering::less; if (x > y) return std::partial_ordering::greater; if (x == y) return std::partial_ordering::equivalent; return std::partial_ordering::unordered; // NaN}Comparison Categories
The result of this operator is neitherbool norint, but a value of comparison category.
| Comparison Category | Example | Possible Values |
|---|---|---|
std::strong_ordering | int | less,equal = equivalent,greater |
std::weak_ordering | user-defined1) | less,equivalent,greater |
std::partial_ordering | float | less,equivalent,greater,unordered |
std::strong_orderings can be converted tostd::weak_ordering, which can be converted tostd::partial_ordering.Values of these categories are comparable to (e.g.(x <=> y) == 0) and this has similar meaning to thecompare function above.However,std::partial_ordering::unordered returns false for all comparisons.
1)There are no fundamental types for whichx <=> y results instd::weak_ordering. Strong and weak orderings are interchangeable in practice; seePractical meaning of std::strong_ordering and std::weak_ordering.
Manual Implementation of Three-Way Comparison
Three-way comparison is often defaulted, but could be implemented manually like:
#include <compare> // necessary, even if we don't use std::is_eqstruct S { int x, y, z; // This implementation is the same as what the compiler would do // if we defaulted <=> with = default; friend constexpr auto operator<=>(const S& l, const S& r) noexcept { // C++17 if statement with declaration makes this more readable. // !std::is_eq(c) is not the same as std::is_neq(c); it is also true // for std::partial_order::unordered. if (auto c = l.x <=> r.x; !std::is_eq(c)) /* 1) */ return c; if (auto c = l.y <=> r.y; !std::is_eq(c)) return c; return l.y <=> r.y; } // == is not automatically defined in terms of <=>. friend constexpr bool operator==(const S&, const S&) = default;};If all members ofS weren't the same type, having anauto return type could be an issue because the comparison category (type of<=>) could be different and the multiplereturn statements would have inconsistent deduction.In that case, we could either specify the category directly (in the return type), or we could obtain it withstd::common_comparison_category:
std::common_comparison_category_t<decltype(l.x <=> l.x), /* ... */>1)Helper functions likestd::is_neq compare the result of<=> to zero.They express intent more clearly, but you don't have to use them.
Common Idioms
Alternatively, we can letstd::tie figure out the details:
#include <tuple>struct S { int x, y, z; friend constexpr auto operator<=>(const S& l, const S& r) noexcept { return std::tie(l.x, l.y, l.z) <=> std::tie(r.x, r.y, r.z); }};Usestd::lexicographical_compare_three_way for array members.
11 Comments
x == y doesnot mean(x <=> y) == 0. Second, given the C++20 expression rewrite symmetry, I don't think the "comparison operators shouldn't be member functions" is a real guideline. Members work just fine for nearly all use-cases. Third, why usestd::is_eq(c) when you, everywhere else, usec == 0?== is provided as a result of defaulting three-way (which is why I've listed that as "meaning", it's not delegated to it. I'll see about rewording that part. 2. It's still the advice I'd give. While it matters less in C++20, it's still more teachable to have the same advice for C++17 and older, and not confuse readers with additional rules that only exist due to rewriting.== is meant to be symmetrical, so make it a free function; simple as. 3. Because in the tables, I'm referring to equivalences in the standard, not to what I'd consider most readable code.std::is_eq(c) to be more readable thanc == 0 - especially when you consider that every example anywhere uses the latter formulation.std::is_eq better. Also the alternative is writing!(c == 0) which someone unknowingly could "optimize" toc != 0. The alternative withstd::is_eq is safer against such hasty uninformed cleanup attempts because it doesn't look intuitively wrong.!(c == 0) andc != 0 mean the same thing, by definition.Exhaustive demo.std::is_eq(c) is really just defined to meanc == 0, dittostd::is_neq(c) is just defined to bec != 0...Summary of Canonical Function Signatures
Many operator overloads can return pretty much anything. For example, nothing stops you from returningvoid inoperator==.However, only a few of these signatures arecanonical, which means that you would normally write them that way, and that such an operator can be explicitly defaulted with= default.
Assignment Operators
struct X { X& operator=(const X&) = default; // copy assignment operator X& operator=(X&&) noexcept = default; // move assignment operator};Explicit defaulting with= default; is possible, but you can also implement assignment manually.Move assignment is almost alwaysnoexcept, although it isn't mandatory.
Comparison Operators
#include <compare> // for comparison categoriesstruct X { friend auto operator<=>(const X&, const X&) = default; // defaulted three-way comparison friend std::strong_ordering<=>(const X&, const X&); // manual three-way comparison friend bool operator==(const X&, const X&) = default; // equality comparisons friend bool operator!=(const X&, const X&) = default; // defaultable since C++20 friend bool operator<(const X&, const X&) = default; // relational comparisons friend bool operator>(const X&, const X&) = default; // defaultable since C++20 friend bool operator<=(const X&, const X&) = default; friend bool operator>=(const X&, const X&) = default;};Seethis answer for more information on when and how to default/implement comparisons.
Arithmetic Operators
struct X { friend X operator+(const X&, const X&); // binary plus friend X operator*(const X&, const X&); // binary multiplication friend X operator-(const X&, const X&); // binary minus friend X operator/(const X&, const X&); // binary division friend X operator%(const X&, const X&); // binary remainder X operator+() const; // unary plus X operator-() const; // unary minus X& operator++(); // prefix increment X& operator--(); // prefix decrement X operator++(int); // postfix increment X operator--(int); // postfix decrement X& operator+=(const X&); // compound arithmetic assignment X& operator-=(const X&); X& operator*(const X&); X& operator/=(const X&); X& operator%=(const X&);};It is also possible to take the left operator of binary operators by value, but this is not recommended because it makes the signature asymmetric and inhibits compiler optimizations.
Bitwise Operators
struct X { using difference_type = /* some integer type */; friend X operator&(const X&, const X&); // bitwise AND friend X operator|(const X&, const X&); // bitwise OR friend X operator^(const X&, const X&); // bitwise XOR friend X operator<<(const X&, difference_type); // bitwise left-shift friend X operator>>(const X&, difference_type); // bitwise right-shift X operator~() const; // bitwise NOT X& operator&=(const X&); // compound bitwise assignment X& operator|=(const X&); X& operator^(const X&); X& operator/=(const X&); X& operator%=(const X&);};Stream Insertion and Extraction
#include <ostream> // std::ostream#include <istream> // std::istreamstruct X { friend std::ostream& operator<<(std::ostream&, const X&); // stream insertion friend std::istream& operator>>(std::istream&, X&); // stream extraction};Function Call Operator
struct X { using result = /* ... */; result operator()(user-defined-args...) /* const / volatile / & / && */; static result operator()(user-defined-args...); // since C++23};Subscript Operator
struct X { using key_type = /* ... */; using value_type = /* ... */; const value_type& operator[](key_type) const; value_type& operator[](key_type); static value_type& operator[](key_type); // since C++23};Note thatoperator[] can accept multiple parameters since C++23.
Member Access Operators
struct X { using value_type = /* ... */; const value_type& operator*() const; // indirection operator value_type& operator*(); const value_type* operator->() const; // arrow operator value_type* operator->();};Pointer-to-Member Operator
struct X { using member_type = /* ... */; using member_pointer_type = /* ... */; const member_type& operator->*(member_pointer_type) const; member_type& operator->*(member_pointer_type);};Address-of Operator
struct X { using address_type = /* ... */; address_type operator&() const; // address-of operator};Logical Operators
struct X { friend X operator&&(const X&, const X&); // logical AND friend X operator||(const X&, const X&); // logical OR friend X operator!(const X&); // logical NOT};Note that these don't returnbool because they only make sense ifX is already a logical type that similar tobool.
User-defined Conversions
struct X { using type = /* ... */; operator type() const; // arbitrary implicit conversion explicit operator bool() const; // explicit/contextual conversion to bool template <typename T> requires /* ... */ // optionally constrained explicit operator T() const; // conversion function template};Coroutine Await
struct X { using awaiter = /* ... */; awaiter operator co_await() const;};Comma Operator
struct X { using pair_type = /* ... */; // often a template to support combination of arbitrary types friend pair_type operator,(const X&, const X&);};Allocation Functions
struct X { // class-specific allocation functions void* operator new(std::size_t); void* operator new[](std::size_t); void* operator new(std::size_t, std::align_val_t); // C++17 void* operator new[](std::size_t, std::align_val_t); // C++17 // class-specific placement allocation functions void* operator new(std::size_t, user-defined-args...); void* operator new[](std::size_t, user-defined-args...); void* operator new(std::size_t, std::align_val_t, user-defined-args...); // C++17 void* operator new[](std::size_t, std::align_val_t, user-defined-args...); // C++17 // class-specific usual deallocation functions void operator delete(void*); void operator delete[](void*); void operator delete(void*, std::align_val_t); // C++17 void operator delete[](void*, std::align_val_t); // C++17 void operator delete(void*, std::size_t); void operator delete[](void*, std::size_t); void operator delete(void*, std::size_t, std::align_val_t); // C++17 void operator delete[](void*, std::size_t, std::align_val_t); // C++17 // class-specific placement deallocation functions void operator delete(void*, user-defined-args...); void operator delete(void*, user-defined-args...); // class-specific usual destroying deallocation functions void operator delete(X*, std::destroying_delete_t); // C++20 void operator delete(X*, std::destroying_delete_t, std::align_val_t); // C++20 void operator delete(X*, std::destroying_delete_t, std::size_t); // C++20 void operator delete(X*, std::destroying_delete_t, std::size_t, std::align_val_t); // C++20};// non-class specific replaceable allocation functions ...void* operator new(std::size_t);void* operator delete(void*);// ...Comments
Making it short and simple, I'll be referring to some points, which I had come over the past week as I was learning Python and C++, OOP and other things, so it goes as follows:
Thearity of the operator can not be modified further than to what it is!
Overloaded operators can only have one default argument which the function call operator rest it cannot.
Only built-in operators can be overloaded, and the rest can't!
For more information, you can refer toRules for operator overloading, which redirects you to the documentation provided byGeeksforGeeks.
Comments
Explore related questions
See similar questions with these tags.











