Defaulting <=> automatically gives ==, !=, <, >, <=, >= for free
C++20 has a new "default comparison" feature setup so that defaulting <=> gives all the others for free. I believe that this has been the major motivation behind the addition of operator<=>.
Adapted from https://en.cppreference.com/w/cpp/language/default_comparisons:
main.cpp
#include <cassert>
#include <set>
struct Point {
int x;
int y;
auto operator<=>(const Point&) const = default;
};
int main() {
Point pt1{1, 1}, pt2{1, 2};
// Just to show it is enough for `std::set`.
std::set<Point> s;
s.insert(pt1);
// All of these are automatically defined for us!
assert(!(pt1 == pt2));
assert( (pt1 != pt2));
assert( (pt1 < pt2));
assert( (pt1 <= pt2));
assert(!(pt1 > pt2));
assert(!(pt1 >= pt2));
}
compile and run:
sudo apt install g++-10
g++-10 -ggdb3 -O0 -std=c++20 -Wall -Wextra -pedantic -o main.out main.cpp
./main.out
Defining your own operator<=>
An equivalent more explicit version of the above would be:
struct Point {
int x;
int y;
auto operator<=>(const Point& other) const {
if (auto cmp = x <=> other.x; cmp != 0)
return cmp;
return y <=> other.y;
}
bool operator==(const Point& other) const {
return this->x == other.x && this->y == other.y;
}
};
and this is how your code should generally look like when you want a custom operator<=>.
This example uses the same algorithm as the default operator<=>, as explained by cppreference as:
The default operator<=> performs lexicographical comparison by successively comparing the base (left-to-right depth-first) and then non-static member (in declaration order) subobjects of T to compute <=>, recursively expanding array members (in order of increasing subscript), and stopping early when a not-equal result is found
In this case, we need to explicitly implement bool operator==(const Point& other) const because if operator<=> is not defaulted (e.g. as given explicitly above), then operator== is not automatically defaulted:
Per the rules for any operator<=> overload, a defaulted <=> overload will also allow the type to be compared with <, <=, >, and >=.
If operator<=> is defaulted and operator== is not declared at all, then operator== is implicitly defaulted.
Note that if <=> is explicitly defined (i.e. not = default), using operator== = default does not call your custom <=> and just uses the same operator== = default that would be defined if you didn't have a <=>. So you likely never want to do that as it would be confusing. For example, the following somewhat contrived example unintuitively passes:
#include <cassert>
struct Point {
int x;
auto operator<=>(const Point& other) const {
return 0;
}
bool operator==(const Point& other) const = default;
};
int main() {
Point pt1{1}, pt2{2};
assert(pt1 != pt2);
}
so we understand that our <=> which marks everything as equal was never called.
You might also be tempted to implement operator== in terms of your custom operator<=> to reduce repetition as in:
struct Point {
int x;
int y;
auto operator<=>(const Point& other) const {
if (auto cmp = x <=> other.x; cmp != 0)
return cmp;
return y <=> other.y;
}
bool operator==(const Point& other) const {
return (*this <=> other) == 0;
}
};
but this could be potentially slower since <=> are potentially slower than == checks for certain types, e.g. consider the case of strings where a size check is possible before full byte comparison for operator==. This performance issue is the reason why if you define operator<=> then the compiler does not use it for operator==.
Before C++20
Before C++20, you could not do something like operator== = default, and defining one operator would not lead to the others being defined, e.g. the following fails to compile with -std=c++17:
#include <cassert>
struct Point {
int x;
int y;
auto operator==(const Point& other) const {
return x == other.x && y == other.y;
};
};
int main() {
Point pt1{1, 1}, pt2{1, 2};
// Do some checks.
assert(!(pt1 == pt2));
assert( (pt1 != pt2));
}
with error:
main.cpp:16:18: error: no match for ‘operator!=’ (operand types are ‘Point’ and ‘Point’)
16 | assert( (pt1 != pt2));
| ~~~ ^~ ~~~
| | |
| Point Point
The above does compile under -std=c++20 however.
Related:
The return type of <=>: std::strong_ordering vs std::weak_ordering vs std::partial_ordering
The return type of <=> is not an int (-1, 0, 1), but rather an object of one of several types, which can then be compared to int, and which give further information about what kind of ordering is implemented. Notably:
When we gave the implementation:
auto operator<=>(const Point& other) const {
if (auto cmp = x <=> other.x; cmp != 0)
return cmp;
return y <=> other.y;
}
auto would have given us std::strong_ordering because that is what <=> returns between two integers as mentioned at https://en.cppreference.com/w/cpp/language/operator_comparison
Otherwise, if the operands have integral type, the operator yields a prvalue of type std::strong_ordering
and this is the semantic that we'd expect for integer points.
partial_ordering is what you get when doing <=> between floats due to the possibility of NaN which is "uncomparable".
TODO concrete examples of weak_ordering and partial_ordering.
Practical meaning of std::strong_ordering and std::weak_ordering suggests that there is currently no example of them making a different in the standard library.
Related: PartialOrdering, StrictWeakOrdering, TotalOrdering, what's the main difference in application
Actually doing stuff with a std::strong_ordering object
std::strong_ordering is a bit picky, but here are some things that you can or can't do with it:
#include <cassert>
#include <compare>
#include <iostream>
#include <string>
int strongOrderingToInt(std::strong_ordering ord) {
if (ord == std::strong_ordering::equal) return 0;
if (ord == std::strong_ordering::less) return -1;
return 1;
}
int main() {
std::string aa = "aa", ab = "ab";
// OK.
assert(((aa <=> aa) == 0));
assert(((aa <=> ab) < 0));
assert(((ab <=> aa) > 0));
assert((aa <=> aa) == std::strong_ordering::equal);
assert((aa <=> ab) == std::strong_ordering::less);
assert((ab <=> aa) == std::strong_ordering::greater);
std::cout << strongOrderingToInt(aa <=> aa) << std::endl;
std::cout << strongOrderingToInt(aa <=> ab) << std::endl;
std::cout << strongOrderingToInt(ab <=> aa) << std::endl;
// Compilation errors.
//((aa <=> aa) == 1));
//std::cout << (aa <=> aa) std::endl;
}
Notably, the only int that you can compare std::strong_ordering to is 0, any other blows up, this is mentioned e.g. at: https://en.cppreference.com/w/cpp/utility/compare/strong_ordering
The behavior of a program that attempts to compare a strong_ordering with anything other than the integer literal 0 is undefined.
Tested on Ubuntu 24.04, GCC 13.2.0.