The C++20 Standard: An Overview of New C++ Features. Part 2 “Operation ‘Spaceship’”

David Godfrey
8 min readJun 30, 2022

--

This is the second part going over the spaceship operator and how to succinctly write comparison operators.

  1. Modules and a brief history of C++.
  2. Operation Spaceship.
  3. Concepts.
  4. Ranges.
  5. Coroutines.
  6. Other kernel and standard library features. Conclusion.

Operation spaceship

C++ now has its own spaceship!

Motivation

C++ has six comparison operators:

  1. <,
  2. >,
  3. ≤,
  4. ≥,
  5. =,
  6. !=.

They are all expressed through any one inequality. But you still have to write these operations. And this is the problem that the “spaceship” solves.

Suppose you have defined a structure containing a single number:

struct X {
int a;
};

We want to make sure that the values ​​of this structure can be compared with each other. To do this, you have to write six operations:

bool operator== (X l, X r) { return l.a == r.a; }
bool operator!= (X l, X r) { return l.a != r.a; }
bool operator>= (X l, X r) { return l.a >= r.a; }
bool operator<= (X l, X r) { return l.a <= r.a; }
bool operator< (X l, X r) { return l.a < r.a; }
bool operator> (X l, X r) { return l.a > r.a; }

Now imagine that we want to compare the elements of this structure not only with each other, but also with integers. The number of operations increases from six to 18:

bool operator== (X l, int r) { return l.a == r; }
bool operator!= (X l, int r) { return l.a != r; }
bool operator>= (X l, int r) { return l.a >= r; }
bool operator<= (X l, int r) { return l.a <= r; }
bool operator< (X l, int r) { return l.a < r; }
bool operator> (X l, int r) { return l.a > r; }
bool operator== (int l, X r) { return l == r.a; }
bool operator!= (int l, X r) { return l != r.a; }
bool operator>= (int l, X r) { return l >= r.a; }
bool operator<= (int l, X r) { return l <= r.a; }
bool operator< (int l, X r) { return l < r.a; }
bool operator> (int l, X r) { return l > r.a; }

What to do? You can call the stormtroopers. There are many of them, and they will quickly write 18 operations.

Or use the “spaceship”. This new C++ operation is called “spaceship” because it looks like one: <=>. The more formal name “three-way comparison” appears in the documents of the Standard.

Example

I added only one line to the structure Xthat defines the operation <=>. Note that I didn't even write what exactly it does:

#include <iostream>struct X {
auto operator<=>(const X&) const = default; // <-- !
int a;
};

And C++ did everything for me. This will work in more complex cases too, such as when X has multiple fields and base classes. In this case, everything that is in Xmust support comparison. After I have written this magic line, I can compare objects Xin any way:

int main() {
X x1{1}, x42{42};
std::cout << (x1 < x42 ? "x1 < x42" : "not x1 < x42") << std::endl;
std::cout << (x1 > x42 ? "x1 > x42" : "not x1 > x42") << std::endl;
std::cout << (x1 <= x42 ? "x1 <= x42" : "not x1 <= x42") << std::endl;
std::cout << (x1 >= x42 ? "x1 >= x42" : "not x1 >= x42") << std::endl;
std::cout << (x1 == x42 ? "x1 == x42" : "not x1 == x42") << std::endl;
std::cout << (x1 != x42 ? "x1 != x42" : "not x1 != x42") << std::endl;
}

The program is correct. It can be assembled and run. The text output looks like this:

x1 < x42
not x1 > x42
x1 <= x42
not x1 >= x42
not x1 == x42
x1 != x42

The spaceship operation will also work for comparing a structure element Xwith a number. But you have to write an implementation. This time, C++ won't be able to come up with it for you. In the implementation, we will use the built-in operation <=> for numbers:

#include <iostream>struct X {
auto operator<=>(const X&) const = default;
auto operator<=>(int r) const { // <-- !
return this->a <=> r;
}
int a;
};

True, there is a problem. C++ will not create all operations. If you defined this operation not through default but wrote it yourself, the check for equality and inequality will not be added. Who knows the reasons - write in the comments.

int main() {
X x1{1}, x42{42};
std::cout << (x1 < 42 ? "x1 < 42" : "not x1 < 42") << std::endl;
std::cout << (x1 > 42 ? "x1 > 42" : "not x1 > 42") << std::endl;
std::cout << (x1 <= 42 ? "x1 <= 42" : "not x1 <= 42") << std::endl;
std::cout << (x1 >= 42 ? "x1 >= 42" : "not x1 >= 42") << std::endl;
std::cout << (x1 == 42 ? "x1 == 42" : "not x1 == 42") << std::endl; // <--- error
std::cout << (x1 != 42 ? "x1 != 42" : "not x1 != 42") << std::endl; // <--- error
}

However, no one forbids you to define this operation yourself. Another C++20 innovation: you can add a check for equality only, and the inequality will be added automatically:

#include <iostream>struct X {
auto operator<=>(const X&) const = default;
bool operator==(const X&) const = default;
auto operator<=>(int r) const {
return this->a <=> r;
}
bool operator==(int r) const { // <-- !
return operator<=>(r) == 0;
}
int a;
};
int main() {
X x1{1}, x42{42};
std::cout << (x1 < 42 ? "x1 < 42" : "not x1 < 42") << std::endl;
std::cout << (x1 > 42 ? "x1 > 42" : "not x1 > 42") << std::endl;
std::cout << (x1 <= 42 ? "x1 <= 42" : "not x1 <= 42") << std::endl;
std::cout << (x1 >= 42 ? "x1 >= 42" : "not x1 >= 42") << std::endl;
std::cout << (x1 == 42 ? "x1 == 42" : "not x1 == 42") << std::endl;
std::cout << (x1 != 42 ? "x1 != 42" : "not x1 != 42") << std::endl;
}

Although 2 operations had to be defined, it is much better than 18.

We have added code for situations where the left operand is Xand the right operand is int. It turns out that there is no need to write a comparison in the other direction, it will be added automatically:

#include <iostream>struct X {
auto operator<=>(const X&) const = default;
bool operator==(const X&) const = default;
auto operator<=>(int r) const {
return this->a <=> r;
}
bool operator==(int r) const { // <-- !
return operator<=>(r) == 0;
}
int a;
};
int main() {
X x1{1}, x42{42};
std::cout << (1 < x42 ? "1 < x42" : "not 1 < x42") << std::endl;
std::cout << (1 > x42 ? "1 > x42" : "not 1 > x42") << std::endl;
std::cout << (1 <= x42 ? "1 <= x42" : "not 1 <= x42") << std::endl;
std::cout << (1 >= x42 ? "1 >= x42" : "not 1 >= x42") << std::endl;
std::cout << (1 == x42 ? "1 == x42" : "not 1 == x42") << std::endl;
std::cout << (1 != x42 ? "1 != x42" : "not 1 != x42") << std::endl;
}

Theory

On this story about the spaceship could be completed, but it turns out that not everything is so simple. There are a few more nuances.

First, not everything I said is true. No comparison operations were actually added. If you try to explicitly call the less than operator, the compiler will say, “Error. There is no such operation.” Even though the comparison works, getting the address of the “less than” operation will not work:

#include <iostream>struct X {
auto operator<=>(const X&) const = default;
int a;
};
int main() {
X x1{1}, x42{42};
std::cout << (x1.operator<(x42) ? "<" : "!<") // <--- error
<< std::endl;
}

It’s amazing how the compiler performs an operation that doesn’t exist. This is due to the fact that the rules of compiler behavior have changed when calculating comparison operations. When you write x1 < x2, the compiler checks for the presence of the <. But now, if it did not find it, it will definitely look at the "spaceship" operation. It’s in the example, so it uses it. In this case, if the operand types are different, the compiler will look at the comparison in both directions: first in one direction, then in the other. Therefore, there is no need to define a third "spaceship" for comparing intand type X- it is enough to define only the option where X is on the left.

If for some reason you like to write x < yinstead of x.operator<(y), then define the operation <explicitly. I have good news for you: you don't have to write the implementation. defaultwill work for normal comparison operations in the same way as for <=>. Write it and C++ will figure it out for you. In general, C++20 does a lot for you.

#include <iostream>struct X {
auto operator<=>(const X&) const = default;
bool operator<(const X&) const = default; // <-- !
int a;
};
int main() {
X x1{1}, x42{42};
std::cout << (x1.operator<(x42) ? "<" : "!<")
<< std::endl;
}

Note that y operator<required an explicit return type, bool. And <=>this work was provided to the compiler by specifying auto. It means that I don't want to write a type: the compiler is smart, it will understand itself what needs to be put instead of auto. But there is some type there - the function must return something.

It turns out that everything is not so simple. It's not like for simple bool comparison operations. There are three options here. These options are different types of ordering:

  • std::strong_ordering. A linear order whose equal elements are indistinguishable. Examples: int, char, string.
  • std::weak_ordering. Linear order, equals can be distinguished. Examples: string, compared case insensitive; the order on the points of the plane, determined by the distance from the center.
  • std::partial_ordering. Partial order. Examples: float, double, order by inclusion on objects in aset.

Mathematicians are well acquainted with these concepts, there is not even any programming involved. For the rest, I’ll tell you. A linear order is one in which any two elements can be compared with each other. An example of a linear order is integers: no matter what two numbers we take, they can be compared with each other.

With partial ordering, elements can be incomparable. Floating point numbers are floatalso doublesubject to the concept of partial order because they have a special NaN value that cannot be compared to any other number.

Further discussion of ordering is beyond the scope of this article. I just want to say that not everything is as trivial as it seems. I recommend experimenting with partial ordering in different algorithms and containers like set.

Status

“Spaceship” is already everywhere, and you can use it:

  • GCC. Well supported since version 10, although not completely. Full support is promised only in GCC11.
  • Clang. Full support in version 10.
  • Visual studio. Full support in VS 2019.

Conclusion

We have analyzed the “spaceship” operation in some detail, but still some of its functions remained uncovered. For example, an interesting question is how the compiler will handle the situation in which several comparison operators with different types are defined.

--

--