C++20: The Three-Way Comparison Operator

Contents[Show]

The three-way comparison operator <=> is often just called spaceship operator. The spaceship operator determines for two values A  and B whether A < B, A = B, or A > B. You can define the spaceship operator or the compiler can auto-generate it for you.

To appreciate the advantages of the three-way comparison operator, let me start classical.

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:

Honestly, MyInt is an unintuitive type. When you define one of the six ordering relations, you should define all of them. 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.

Modernes C++ Mentoring

Stay informed about my mentoring programs.

Subscribe via E-Mail.

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 by the compiler deduced return type for MyInt (1) supports strong ordering, and the by 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.

Now, 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 in this case that all base classes are compared left to right and all non-static members of the class in their declaration order. I have to qualify: for performance reasons, the compiler-generated == and != operator 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 to 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];
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. In the end, you get the 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, Animus24, Jozo Leko, John Breland, Venkat Nandam, Jose Francisco, Douglas Tinkham, Kuchlong Kuchlong, Robert Blanch, Truels Wissneth, Kris Kafka, 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, Matthieu Bolt, Stephen Kelley, Kyle Dean, Tusar Palauri, Dmitry Farberov, Juan Dent, George Liao, Daniel Ceperley, Jon T Hess, Stephen Totten, Wolfgang Fütterer, Matthias Grün, Phillip Diekmann, Ben Atakora, and Ann Shatoff.

Thanks in particular to Jon Hess, Lakshman, Christian Wittenhorst, Sherhy Pyton, Dendi Suhubdy, Sudhakar Belagurusamy, Richard Sargeant, Rusty Fleming, John Nebel, Mipko, Alicja Kaminska, and Slavko Radman.

My special thanks to Embarcadero

My special thanks to PVS-Studio

My special thanks to Tipi.build

Seminars

I'm happy to give online seminars or face-to-face seminars worldwide. Please call me if you have any questions.

Standard Seminars (English/German)

Here is a compilation of my standard seminars. These seminars are only meant to give you a first orientation.

• C++ - The Core Language
• C++ - The Standard Library
• C++ - Compact
• C++11 and C++14
• Concurrency with Modern C++
• Design Pattern and Architectural Pattern with C++
• Embedded Programming with Modern C++
• Generic Programming (Templates) with C++

New

• Clean Code with Modern C++
• C++20

Modernes C++,

Tags: Spaceship

-7 #1 Pierre Gradot 2020-11-01 10:28
Hi!

You says that:

"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: ==, !=, =."

But the following code won't compile both clang 11.0.0 and gcc 10.2:

#include

struct Foo {
const int value;

auto operator(const Foo& other) const {
return value other.value;
}
};

#include

int main() {
Foo a{42};
Foo b{66};

std::cout

Subscribe to the newsletter (+ pdf bundle)

 Email Please enable the javascript to submit this form

Visitors

Today 2747

Yesterday 5317

Week 2747

Month 146918

All 11628072

Currently are 277 guests and no members online

• How can you recognise a good software architecture?

Code should be as expressive as possible and ideally should be readable without any comments. As ...

• How can you recognise a good software architecture?

I began developing software in the late 1960's and have been at it ever since. The one thing missing ...

• The C++ Standard Library: The Fourth Edition includes C++23

Hi, In India from where do I buy hard copy of The C++ Standard Library: The Fourth Edition includes C++23 ...

• The Iterator Protocol

I think an std::forward_iterator has to return a reference from it's operator* (). This would mean a ...

• An Interview that went Viral

Very well said Rainer. Plus, look at C. C should also be an example of how some languages are too ...