Let's try C++20 | comparisons
C++20 comes with new possibilities to compare objects.
Let's start with this simple code where we try to compare 2 objects of user-defined type:
#include <iostream>
struct Foo {
const int value;
};
int main() {
Foo a{42};
Foo b{66};
std::cout << std::boolalpha;
std::cout << (a == b) << '\n';
std::cout << (a != b) << '\n';
std::cout << (a > b) << '\n';
std::cout << (a >= b) << '\n';
std::cout << (a < b) << '\n';
std::cout << (a <= b) << '\n';
}
This code doesn't compile, no matter which version of C++ you are using. Indeed, none of these operators are implicitly defined by the compiler and we get 6 error (one per operator):
prog.cc: In function 'int main()':
prog.cc:12:21: error: no match for 'operator==' (operand types are 'Foo' and 'Foo')
12 | std::cout << (a == b) << '\n';
| ~ ^~ ~
| | |
| Foo Foo
prog.cc:13:21: error: no match for 'operator!=' (operand types are 'Foo' and 'Foo')
13 | std::cout << (a != b) << '\n';
| ~ ^~ ~
| | |
| Foo Foo
prog.cc:15:21: error: no match for 'operator>' (operand types are 'Foo' and 'Foo')
15 | std::cout << (a > b) << '\n';
| ~ ^ ~
| | |
| Foo Foo
prog.cc:16:21: error: no match for 'operator>=' (operand types are 'Foo' and 'Foo')
16 | std::cout << (a >= b) << '\n';
| ~ ^~ ~
| | |
| Foo Foo
prog.cc:18:21: error: no match for 'operator<' (operand types are 'Foo' and 'Foo')
18 | std::cout << (a < b) << '\n';
| ~ ^ ~
| | |
| Foo Foo
prog.cc:19:21: error: no match for 'operator<=' (operand types are 'Foo' and 'Foo')
19 | std::cout << (a <= b) << '\n';
| ~ ^~ ~
| | |
| Foo Foo
Until C++17, we had to define all operators manually. We were not even able to default them:
struct Foo {
const int value;
bool operator==(const Foo& other) = default;
};
Compiler error was:
error: defaulted 'bool Foo::operator==(const Foo&)' only available with '-std=c++20' or '-std=gnu++20'
Defaulted operator==()
Let's simply listen to GCC and compile our code with -std=c++20
option:
error: defaulted 'bool Foo::operator==(const Foo&)' must be 'const'
When we add const
, the compiler generates operator==()
as requested and it implicitly defines operator=!()
. In fact, the later is also implicitly defined if the former is user-defined.
Consequence: the 6 errors about missing operators are reduced to 4. operator==()
and operator=!()
work as expected:
int main() {
Foo a{42};
Foo b{66};
std::cout << std::boolalpha;
std::cout << (a == b) << '\n';
std::cout << (a != b) << '\n';
}
Output:
false
true
What does this defaulted operator==()
do? As always, cppreference has the answer. "Compiler generates element-wise equality testing" for this operator:
A class can define
operator==
as defaulted, with a return value ofbool
. This will generate an equality comparison of each base class and member subobject, in their declaration order. Two objects are equal if the values of their base classes and members are equal. The test will short-circuit if an inequality is found in members or base classes earlier in declaration order.
Try to Default Other Operators
You may be tempted to default other comparison operators. Spoiler alert: it won't work (*). From the 6 operators, only operator()==
can be defaulted.
#include <iostream>
struct Foo {
const int value;
bool operator>(const Foo& other) const = default;
};
int main() {
Foo a{42};
Foo b{66};
std::cout << std::boolalpha;
std::cout << (a > b) << '\n';
}
Errors are:
prog.cc: In function 'int main()':
prog.cc:14:23: error: use of deleted function 'bool Foo::operator>(const Foo&) const'
14 | std::cout << (a > b) << '\n';
| ^
prog.cc:6:10: note: 'bool Foo::operator>(const Foo&) const' is implicitly deleted because the default definition would be ill-formed:
6 | bool operator>(const Foo& other) const = default;
| ^~~~~~~~
prog.cc:6:10: error: no match for 'operator<=>' (operand types are 'const Foo' and 'const Foo')
This is where three-way comparison comes into play.
(*) = cppreference seems to say that it is possible to default all 6 comparison operators, but I got errors with both gcc and clang, except with operator()==
. Seems like they can be defaulted only if operator==
and/or operator<=>
are defined.
Three-way Comparison
There is a new operator in C++20: operator<=>()
. It is called "spaceship operator" and it performs a three-way comparison:
A three-way comparison takes two values A and B belonging to a type with a total order and determines whether A < B, A = B, or A > B in a single operation, in accordance with the mathematical law of trichotomy.
You may not be familiar with the names "spaceship operator" or "three-way comparison" but you have probably experienced them already.
Remember strcmp()
from C?
int strcmp(const char *s1, const char *s2);
Return Value
The
strcmp()
andstrncmp()
functions return an integer less than, equal to, or greater than zero ifs1
(or the first n bytes thereof) is found, respectively, to be less than, to match, or be greater thans2
.
Ever tried the Comparable interface in Java?
Interface
Comparable<T>
int compareTo(T o)
Returns: a negative integer, zero, or a positive integer as this object is less than, equal to, or greater than the specified object.
These are three-way comparisons.
Defaulted operator<=>
Let's get back to our struct Foo
and try to default this spaceship operator. Doing so will also generate other operators:
A class that defines
operator<=>
as defaulted will generate a 3-way comparison element-wise. It will perform 3-way comparisons on each base class and member subobject, in declaration order. These comparisons will short-circuit on the first non-equal base class or member. If the return type of the defaulted function isauto
, then the comparison type will be the most restrictive of all of the 3-way comparison results for its subobjects.Per the rules for any
operator<=>
overload, a defaulted<=>
overload will also allow the type to be compared with<
,<=
,>
, and>=
. Ifoperator<=>
is defaulted andoperator==
is not declared at all, thenoperator==
is implicitly defaulted.
#include <compare> // this is important
struct Foo {
const int value;
auto operator<=>(const Foo&) const = default;
};
#include <iostream>
int main() {
Foo a{42};
Foo b{66};
std::cout << std::boolalpha;
std::cout << (a == b) << '\n';
std::cout << (a != b) << '\n';
std::cout << (a > b) << '\n';
std::cout << (a >= b) << '\n';
std::cout << (a < b) << '\n';
std::cout << (a <= b) << '\n';
}
This code compiles and outputs:
false
true
false
false
true
true
Ain't that great?! 😍
Object Comparison Really Changes in C++20
Early papers for C++20 proposed that operator<=>()
would be used to generate all other 6 operators, including operator==()
. Barry Revzin then writes several papers to change this behavior. See for instance P1630R1: Spaceship needs a tune-up. Eventually, his document P1185R2: <=> != ==
has made it into the standard, as you can see in the section "Three-way comparison (“spaceship”) and defaulted comparisons" on Changes between C++17 and C++20 DIS.
They are many papers in this section and they create a complete change in how we will compare objects in C++.
Primary vs Secondary Operators
The first important change is the new spaceship operator and the possibility to default comparison operators. These operators can be split in 2 groups : on one side, we have equality operators ==
and !=
, and on the other side we have relational operators <=>
, >
, >=
, <
, <=
.
In it's a blog article "Comparisons in C++20", Barry Revzin introduces the names "primary" and "secondary" operators. ==
and <=>
are primary, the other are secondary.
Primary operators can be defaulted. Secondary operators can be defaulted if the corresponding primary operator is defined.
Secondary operators can be implicitly defined by the compiler if the primary operators are user-defined or defaulted. For instance, defaulted !=
generates a call to negated ==
.
Defaulting <=>
will also implicitly default ==
if it is not defined, and hence defaulting all operators.
Defaulted ==
and !=
do not invoke <=>
.
Reversing Comparisons
The second important change is the ability for compilers to reverse operators and operands in comparisons.
To understand this, let's get back to C++17 and consider this code:
struct Foo {
const int value;
bool operator==(int v) const {
return value == v;
}
};
#include <iostream>
int main() {
Foo foo{42};
std::cout << std::boolalpha << (foo == 42);
}
This code compiles fine and prints true
. Unfortunately, std::cout << std::boolalpha << (42 == foo);
doesn't compile:
error: no match for 'operator==' (operand types are 'int' and 'Foo')
The solution is to write a free operator:
bool operator==(int v, Foo foo) {
return foo.value == v;
}
In C++20, you don't have to do that. Both comparisons compile and print true
. Indeed, primary operators can be reversed automatically by the compiler. When the compiler evaluates 42 == foo
but doesn't find a suitable operator, it reverses the operation as foo == 42
and looks again for a suitable operator, which is found in my example.
In the blog article linked above, Barry Revzin says:
In C++20, expressions containing secondary comparison operators will also try to look up their corresponding primary operators and write the secondary comparison in terms of the primary.
Because the language considers reversing candidates, you can write all of these operators as member functions too. No more writing non-member functions just to handle heterogeneous comparison.
Its means that secondary operators cannot be reversed on their own. For instance, the following code doesn't compile (not even in C++20):
struct Foo {
const int value;
bool operator!=(int v) const {
return value == v;
}
};
#include <iostream>
int main() {
Foo foo{42};
std::cout << std::boolalpha << (42 != foo);
}
The error is:
error: no match for 'operator!=' (operand types are 'int' and 'Foo')
Nevertheless, the compiler is able to reverse an operation with a secondary operator by using the corresponding primary operator. Here is a correct code:
struct Foo {
const int value;
bool operator==(int v) const {
return value == v;
}
};
#include <iostream>
int main() {
Foo foo{42};
std::cout << std::boolalpha << (42 != foo);
}
All in all, you just have to write the primary operators and all operations with secondary operators will work. Example:
#include <compare>
struct Foo {
const int value;
bool operator==(int v) const {
return value == v;
}
auto operator<=>(int v) const {
return value <=> v;
}
};
#include <iostream>
int main() {
Foo foo{42};
std::cout << std::boolalpha << (42 != foo) << '\n';
std::cout << std::boolalpha << (42 <= foo) << '\n';
std::cout << std::boolalpha << (42 < foo) << '\n';
std::cout << std::boolalpha << (42 >= foo) << '\n';
std::cout << std::boolalpha << (42 > foo) << '\n';
}
Output:
true
false
true
false
true
false
Strong vs Weak vs Partial Ordering
A last point I have to talk about is the return type of the spaceship operator. As stated in the section "Three-way comparison" of the "Operator Comparison" on cppreference:
The three-way comparison operator expressions have the form
lhs <=> rhs
(1)The expression returns an object such that
(a <=> b) < 0
iflhs < rhs
(a <=> b) > 0
iflhs > rhs
(a <=> b)== 0
iflhs
andrhs
are equal/equivalent.
Nevertheless, this operator doesn't return an integer and this is why the <compare>
header must be included to define it:
//#include <compare>
struct Foo {
const int value;
auto operator<=>(const Foo&) const = default;
};
int main() {
Foo foo{42};
Foo bar{66};
return foo <=> bar;
}
<source>: In member function 'constexpr auto Foo::operator<=>(const Foo&) const':
<source>:6:10: error: 'strong_ordering' is not a member of 'std'
6 | auto operator<=>(const Foo&) const = default;
| ^~~~~~~~
<source>:1:1: note: 'std::strong_ordering' is defined in header '<compare>'; did you forget to '#include <compare>'?
The auto-deduced return type is std::strong_ordering
here and it is defined in <compare>
.
If you uncomment the #include
, you will get another error:
<source>:13:16: error: cannot convert 'std::strong_ordering' to 'int' in return
13 | return foo <=> bar;
| ~~~~^~~~~~~
| |
| std::strong_ordering
The type can be compared to but not converted to an integer. You have to change the return statement:
Statement | Value |
---|---|
return (foo <=> bar) == 0; |
0 |
return (foo <=> foo) == 0; |
1 |
return (bar <=> bar) == 0; |
1 |
return (bar <=> foo) == 0; |
0 |
return (bar <=> foo) > 0; |
1 |
return (bar <=> foo) < 0; |
0 |
return (foo <=> bar) > 0; |
0 |
return (foo <=> bar) < 0; |
1 |
std::strong_ordering
is not always the return type of the spaceship operator. It is also possible that it returns std::weak_ordering or std::partial_ordering.
What are the differences between them?
Here is what Barry Revzin says in its article (you should really read it, it's great 😉):
strong_ordering
: a total ordering, where equality implies substitutability (that is(a <=> b) == strong_ordering::equal
implies that for reasonable functionsf
,f(a) == f(b)
. “Reasonable” is deliberately underspecified – but shouldn’t include functions that return the address of their arguments or do things like return thecapacity()
of avector
, etc. We want to only look at “salient” properties – itself very underspecified, but think of it as referring to the value of a type. The value of avector
is the elements it contains, not its address, etc.). The values arestrong_ordering::greater
,strong_ordering::equal
, andstrong_ordering::less
.
weak_ordering
: a total ordering, where equality actually only defines an equivalence class. The canonical example here is case-insensitive string comparison – where two objects might beweak_ordering::equivalent
but not actually equal (hence the naming change to equivalent).
partial_ordering
: a partial ordering. Here, in addition to the valuesgreater
,equivalent
, andless
(as withweak_ordering
), we also get a new value:unordered
. This gives us a way to represent partial orders in the type system:1.f <=> NaN
ispartial_ordering::unordered
.
Conclusion
When I started to write, I though it would be an easy-to-write article about the spaceship operator. Damned it wasn't! 😨
C++20 really changes the deal with comparison operators. It is now possible to default operators and heterogeneous operations are handled by the compiler. In my opinion, this is a real improvement in the language that will help to write readable, easy-to-use code and reduce bugs.
Top comments (2)
Great to see more C++ content on Dev! What compiler do you use with decent support for C++20? I was advised recently MSVC++, but I'm on Linux, so not a viable option.
Hey! Thanks :)
For my tests, I am using wandbox.org/ or godbolt.org/
Head versions of both GCC and CLANG seem to work properly. See en.cppreference.com/w/cpp/compiler... more for more details about which features is supported by each major compilers ;)