C++20: The Three-Way Comparison Operator
The three-way comparison operator <=> is often just called the spaceship operator. The spaceship operator determines whether A < B, A = B, or A > B for two values, A and B. You can define the spaceship operator, or the compiler can auto-generate it.
Let me start classically to appreciate the three-way comparison operator’s advantages.
Ordering before C++20
I implemented a simple int wrapper MyInt. Of course, I want to compare MyInt. Here is my solution using the isLessThan function template.
// comparisonOperator.cpp #include <iostream> struct MyInt { int value; explicit MyInt(int val): value{val} { } bool operator < (const MyInt& rhs) const { return value < rhs.value; } }; template <typename T> constexpr bool isLessThan(const T& lhs, const T& rhs) { return lhs < rhs; } int main() { std::cout << std::boolalpha << std::endl; MyInt myInt2011(2011); MyInt myInt2014(2014); std::cout << "isLessThan(myInt2011, myInt2014): " << isLessThan(myInt2011, myInt2014) << std::endl; std::cout << std::endl; }
The program works as expected:
Modernes C++ Mentoring
Do you want to stay informed: Subscribe.
Honestly, MyInt is an unintuitive type. When you define one of the six ordering relations, you should define all. Intuitive types should be at least semi-regular: “C++20: Define the Concept Regular and SemiRegular.“
Now, I have to write a lot of boilerplate code. Here are the missing five operators:
bool operator==(const MyInt& rhs) const { return value == rhs.value; } bool operator!=(const MyInt& rhs) const { return !(*this == rhs); } bool operator<=(const MyInt& rhs) const { return !(rhs < *this); } bool operator>(const MyInt& rhs) const { return rhs < *this; } bool operator>=(const MyInt& rhs) const { return !(*this < rhs); }
Done? No! I assume you want to compare MyInt with int’s. To support the comparison of an int and a MyInt, and a MyInt and an int, you have to overload each operator three times because the constructor is declared as explicit. Thanks to explicit, no implicit conversion from int to MyInt kicks in. For convenience, you make the operators to a friend of the class. If you need more background information for my design decisions, read my previous post: “C++ Core Guidelines: Rules for Overloading and Overload Operators“
These are the three overloads for smaller-than.
friend bool operator < (const MyInts& lhs, const MyInt& rhs) { return lhs.value < rhs.value; } friend bool operator < (int lhs, const MyInt& rhs) { return lhs < rhs.value; } friend bool operator < (const MyInts& lhs, int rhs) { return lhs.value < rhs; }
This means, in total, that you have to implement 18 comparison operators. Is this the end of the story? Maybe not, because you decided that the MyInt and all operators should become constexpr. You should also consider making the operators noexcept.
I assume this is enough motivation for the three-way comparison operators.
Ordering with C++20
You can define the three-way comparison operator or request it from the compiler with =default. In both cases, you get all six comparison operators: ==, !=, <, <=, >, and >=.
// threeWayComparison.cpp #include <compare> #include <iostream> struct MyInt { int value; explicit MyInt(int val): value{val} { } auto operator<=>(const MyInt& rhs) const { // (1) return value <=> rhs.value; } }; struct MyDouble { double value; explicit constexpr MyDouble(double val): value{val} { } auto operator<=>(const MyDouble&) const = default; // (2) }; template <typename T> constexpr bool isLessThan(const T& lhs, const T& rhs) { return lhs < rhs; } int main() { std::cout << std::boolalpha << std::endl; MyInt myInt1(2011); MyInt myInt2(2014); std::cout << "isLessThan(myInt1, myInt2): " << isLessThan(myInt1, myInt2) << std::endl; MyDouble myDouble1(2011); MyDouble myDouble2(2014); std::cout << "isLessThan(myDouble1, myDouble2): " << isLessThan(myDouble1, myDouble2) << std::endl; std::cout << std::endl; }
The user-defined (1) and the compiler-generated (2) three-way comparison operator work as expected.
But there are a few subtle differences in this case. The compiler-deduced return type for MyInt (1) supports strong ordering, and the compiler-deduced return type of MyDouble supports partial ordering. Floating pointer numbers only support partial ordering because floating-point values such as NaN (Not a Number) can not be ordered. For example, NaN == NaN is false.
I want to focus on this post about the compiler-generated spaceship operator.
The Compiler-Generated Spaceship Operator
The compiler-generated three-way comparison operator needs the header <compare>, which is implicit constexpr and noexcept. Additionally, it performs a lexicographical comparison. What? Let me start with constexpr.
Comparison at Compile-Time
The three-way comparison operator is implicit constexpr. Consequently, I simplify the previous program threeWayComparison.cpp and compare MyDouble in the following program at compile-time.
// threeWayComparisonAtCompileTime.cpp #include <compare> #include <iostream> struct MyDouble { double value; explicit constexpr MyDouble(double val): value{val} { } auto operator<=>(const MyDouble&) const = default; }; template <typename T> constexpr bool isLessThan(const T& lhs, const T& rhs) { return lhs < rhs; } int main() { std::cout << std::boolalpha << std::endl; constexpr MyDouble myDouble1(2011); constexpr MyDouble myDouble2(2014); constexpr bool res = isLessThan(myDouble1, myDouble2); // (1) std::cout << "isLessThan(myDouble1, myDouble2): " << res << std::endl; std::cout << std::endl; }
I ask for the result of the comparison at compile-time (1), and I get it.
The compiler-generated three-way comparison operator performs a lexicographical comparison.
Lexicographical Comparison
Lexicographical comparison means that all base classes are compared left to right and all non-static members in their declaration order. I have to qualify: for performance reasons, the compiler-generated == and != operators behave differently in C++20. I will write about this exception to the rule in my next post.
The post “Simplify Your Code With Rocket Science: C++20’s Spaceship Operator” Microsoft C++ Team Blog provides an impressive example of the lexicographical comparison.
struct Basics { int i; char c; float f; double d; auto operator<=>(const Basics&) const = default; }; struct Arrays { int ai[1]; char ac[2]; float af[3]; double ad[2][2]; auto operator<=>(const Arrays&) const = default; }; struct Bases : Basics, Arrays { auto operator<=>(const Bases&) const = default; }; int main() { constexpr Bases a = { { 0, 'c', 1.f, 1. }, // (1) { { 1 }, { 'a', 'b' }, { 1.f, 2.f, 3.f }, { { 1., 2. }, { 3., 4. } } } }; constexpr Bases b = { { 0, 'c', 1.f, 1. }, // (1) { { 1 }, { 'a', 'b' }, { 1.f, 2.f, 3.f }, { { 1., 2. }, { 3., 4. } } } }; static_assert(a == b); static_assert(!(a != b)); static_assert(!(a < b)); static_assert(a <= b); static_assert(!(a > b)); static_assert(a >= b); }
I assume the most complex aspect of the program is not the spaceship operator but the initialization of Base via aggregate initialization (1). Aggregate initialization enables it to directly initialize the members of a class type (class, struct, union) when the members are all public. In this case, you can use brace initialization. If you want to know more about aggregate initialization, cppreference.com provides more information. I will write more about aggregate initialization in a future post when I will have a closer look at designated initialization in C++20.
What’s next?
The compiler performs quite a clever job when it generates all operators. Ultimately, you get intuitive and efficient comparison operators for free. My next post dives deeper into the magic under the hood.
Thanks a lot to my Patreon Supporters: Matt Braun, Roman Postanciuc, Tobias Zindl, G Prvulovic, Reinhold Dröge, Abernitzke, Frank Grimm, Sakib, Broeserl, António Pina, Sergey Agafyin, Андрей Бурмистров, Jake, GS, Lawton Shoemake, Jozo Leko, John Breland, Venkat Nandam, Jose Francisco, Douglas Tinkham, Kuchlong Kuchlong, Robert Blanch, Truels Wissneth, Mario Luoni, Friedrich Huber, lennonli, Pramod Tikare Muralidhara, Peter Ware, Daniel Hufschläger, Alessandro Pezzato, Bob Perry, Satish Vangipuram, Andi Ireland, Richard Ohnemus, Michael Dunsky, Leo Goodstadt, John Wiederhirn, Yacob Cohen-Arazi, Florian Tischler, Robin Furness, Michael Young, Holger Detering, Bernd Mühlhaus, Stephen Kelley, Kyle Dean, Tusar Palauri, Juan Dent, George Liao, Daniel Ceperley, Jon T Hess, Stephen Totten, Wolfgang Fütterer, Matthias Grün, Phillip Diekmann, Ben Atakora, Ann Shatoff, Rob North, Bhavith C Achar, Marco Parri Empoli, Philipp Lenk, Charles-Jianye Chen, Keith Jeffery,and Matt Godbolt.
Thanks, in particular, to Jon Hess, Lakshman, Christian Wittenhorst, Sherhy Pyton, Dendi Suhubdy, Sudhakar Belagurusamy, Richard Sargeant, Rusty Fleming, John Nebel, Mipko, Alicja Kaminska, Slavko Radman, and David Poole.
My special thanks to Embarcadero | |
My special thanks to PVS-Studio | |
My special thanks to Tipi.build | |
My special thanks to Take Up Code | |
My special thanks to SHAVEDYAKS |
Modernes C++ GmbH
Modernes C++ Mentoring (English)
Rainer Grimm
Yalovastraße 20
72108 Rottenburg
Mail: schulung@ModernesCpp.de
Mentoring: www.ModernesCpp.org
Modernes C++ Mentoring,
Thanks!
Thank you for the article. You wrote:
“You can define the three-way comparison operator or request it from the compiler with =default. In both cases, you get all six comparison operators: ==, !=, <, , and >=.”
I was under the *same* expression. But it turned out to be wrong. operator== and operator!= are never rewritten in terms of operator. It’s just that “defaulting” operator will implicitly “default” operator== as well. So, if you write your own operator implementation you would also have to add your own operator== implementation to be able to apply == and != operators.
I believe this was changed in some revision because it was recognized that an operator== or operator!= would be potentially less efficient when implemented in terms of operator. I guess the conclusion was to not have code compile rather than being inefficient.
But this special implicit =default; rule for operator== is something that might confuse people into thinking operator is all that matters.