New spaceship operator in C ++ 20
- Transfer
C ++ 20 adds a new operator, called the "spaceship":

It is not unusual to see code like the following:
Note: careful readers will notice that this is actually even less verbose than it should be in code before C ++ 20. More on this later.
You need to write a lot of standard code to make sure that our type is comparable to something of the same type. Well, we will deal with this for some time. Then comes someone who writes like this:
The first thing you notice is that the program will not compile.
The problem is that it was forgotten
This is where the new C ++ 20 spaceship operator comes to help us. Let's see how you can write the source
The first difference you may notice is the new inclusion
We not only deleted 5 extra lines, but we don’t even need to determine anything, the compiler will do it for us.
In C ++ 20, the compiler is introduced into a new concept related to “rewritten” expressions. The spaceship operator, along with
While resolving the overload, the compiler will choose from a set of the most suitable candidates, each of which corresponds to the operator that we need. The selection process changes very slightly for comparison operations and equivalence operations, when the compiler must also collect special transcribed and synthesized candidates ( [over.match.oper] /3.4 ).
For our expression, the
You may ask why this rewritten expression is correct. The correctness of the expression actually follows from the semantics that the spaceship operator provides.
Using the above information, the compiler can take any generalized comparison operator (ie
Readers may have noticed a subtle reference to the “synthesized” expressions above, and they also play a role in this process of rewriting statements. Consider the following function:
If we use our original definition for
This makes sense prior to C ++ 20, and the way to solve this problem is to add some additional features
While resolving overloads, the compiler will also collect what the standard calls “synthesized” candidates, or a rewritten expression with the reverse order of parameters. In the above example, the compiler will try to use a rewritten expression
The purpose of synthesized expressions is to avoid confusion about the need to write function templates
The spaceship operator generated by the compiler does not stop at individual members of classes, it generates the correct set of comparisons for all subobjects in your types:
The compiler knows how to expand class members, which are arrays, into their lists of subobjects and compare them recursively. Of course, if you want to write the bodies of these functions yourself, you will still benefit from rewriting expressions by the compiler.
Looks like a duck, swims like a duck, and quacks like
Some very smart people on the standardization committee have noticed that the spaceship operator will always perform a lexicographic comparison of elements, no matter what. Unconditional execution of lexicographic comparisons can lead to inefficient code, in particular, with the equality operator.
A canonical example comparing two lines. If you have a string
In accordance with the rules of the spaceship operator, we must start by comparing each element until we find one that is different. In our example,
To combat this, there was an article P1185R2 that details how the compiler rewrites and generates
One more step ... however, there is good news; you don’t really need to write the code above, because just writing it is
The compiler applies a slightly modified “rewrite” rule, specific to
At this point, you might think: well, if the compiler is allowed to perform this operator rewriting operation, what will happen if I try to outwit the compiler:
The answer is no big deal. The overload resolution model in C ++ is the arena that all candidates face. In this particular battle, we have three of them:
(rewritten)
(synthesized)
If we adopted overload resolution rules in C ++ 17, the result of this call would be mixed, but the C ++ 20 overload resolution rules were changed so that the compiler could resolve this situation to the most logical overload.
There is an overload resolution phase when the compiler must complete a series of extra passes. A new mechanism has appeared in C ++ 20, within which preference is given to overloads that are not rewritten or synthesized, which makes our overload the
The spaceship operator is a welcome addition to C ++, as it can help simplify your code and write less, and sometimes less is better. So buckle up and control your C ++ 20 spaceship !
We urge you to go out and try out the spaceship operator, it is available right now in Visual Studio 2019 under
As always, we await your feedback. Feel free to send any comments via email to visualcpp@microsoft.com, via Twitter @visualc , or Facebook Microsoft Visual Cpp .
If you encounter other problems with MSVC in VS 2019, please let us know via the “Report a Problem” option , either from the installer or from the Visual Studio IDE itself. For suggestions or bug reports, write to us through DevComm.
<=>
. Not so long ago, Simon Brand published a post which contained detailed conceptual information about what this operator is and for what purposes it is used. The main objective of this post is to study the specific applications of the “strange” new operator and its analogue operator==
, as well as formulate some recommendations for its use in everyday coding.
Comparison
It is not unusual to see code like the following:
structIntWrapper {
int value;
constexprIntWrapper(int value): value{value} { }
booloperator==(const IntWrapper& rhs) const { return value == rhs.value; }
booloperator!=(const IntWrapper& rhs) const { return !(*this == rhs); }
booloperator<(const IntWrapper& rhs) const { return value < rhs.value; }
booloperator<=(const IntWrapper& rhs) const { return !(rhs < *this); }
booloperator>(const IntWrapper& rhs) const { return rhs < *this; }
booloperator>=(const IntWrapper& rhs) const { return !(*this < rhs); }
};
Note: careful readers will notice that this is actually even less verbose than it should be in code before C ++ 20. More on this later.
You need to write a lot of standard code to make sure that our type is comparable to something of the same type. Well, we will deal with this for some time. Then comes someone who writes like this:
constexprboolis_lt(const IntWrapper& a, const IntWrapper& b){
return a < b;
}
intmain(){
static_assert(is_lt(0, 1));
}
The first thing you notice is that the program will not compile.
error C3615: constexpr function 'is_lt' cannot result in a constant expression
The problem is that it was forgotten
constexpr
in the comparison function. Then some will add constexpr
to all comparison operators. A few days later, someone will add an assistant is_gt
, but notice that all comparison operators do not have an exception specification, and you will have to go through the same tedious process of adding noexcept
to each of the 5 overloads. This is where the new C ++ 20 spaceship operator comes to help us. Let's see how you can write the source
IntWrapper
in the C ++ 20 world:#include<compare>structIntWrapper {
int value;
constexprIntWrapper(int value): value{value} { }
autooperator<=>(const IntWrapper&) const = default;
};
The first difference you may notice is the new inclusion
<compare>
. The header <compare>
is responsible for populating the compiler with all types of comparison categories necessary for the spaceship operator, so that it returns a type suitable for our default function. In the above snippet, the return type auto
will be std::strong_ordering
. We not only deleted 5 extra lines, but we don’t even need to determine anything, the compiler will do it for us.
is_lt
It remains unchanged and just works, remaining at the same time constexpr
, although we did not explicitly indicate this in our default operator<=>
. This is good, but some people may puzzle over why it is is_lt
allowed to compile, even if it does not use the spaceship operator at all. Let's find the answer to this question.Rewriting Expressions
In C ++ 20, the compiler is introduced into a new concept related to “rewritten” expressions. The spaceship operator, along with
operator==
, is one of the first two candidates that can be rewritten. For a more specific example of rewriting expressions, let's look at the example given in is_lt
. While resolving the overload, the compiler will choose from a set of the most suitable candidates, each of which corresponds to the operator that we need. The selection process changes very slightly for comparison operations and equivalence operations, when the compiler must also collect special transcribed and synthesized candidates ( [over.match.oper] /3.4 ).
For our expression, the
a < b
standard states that we can search for a typea
for operator<=>
or functions operator<=>
that accept this type. This is what the compiler does and discovers what the type actually a
contains IntWrapper::operator<=>
. The compiler is then allowed to use this operator and rewrite the expression a < b
as (a <=> b) < 0
. This rewritten expression is then used as a candidate for normal overload resolution. You may ask why this rewritten expression is correct. The correctness of the expression actually follows from the semantics that the spaceship operator provides.
<=>
- This is a three-way comparison, which implies that you get not just a binary result, but also an order (in most cases). If you have an order, you can express this order in terms of any comparison operations. A quick example, expression 4 <=> 5 in C ++ 20 will return you the result std::strong_ordering::less
. The result std::strong_ordering::less
implies that it is 4
not only different from 5
but also strictly less than this value, which makes the application of the operation (4 <=> 5) < 0
correct and accurate to describe our result. Using the above information, the compiler can take any generalized comparison operator (ie
<
, >
,, etc.) and rewrite it in terms of the spaceship operator. In the standard, a rewritten expression is often referred to as (a <=> b) @ 0
where@
represents any comparison operation.Synthesizing Expressions
Readers may have noticed a subtle reference to the “synthesized” expressions above, and they also play a role in this process of rewriting statements. Consider the following function:
constexprboolis_gt_42(const IntWrapper& a){
return42 < a;
}
If we use our original definition for
IntWrapper
, this code will not compile. error C2677: binary '<': no global operator found which takes type 'const IntWrapper' (or there is no acceptable conversion)
This makes sense prior to C ++ 20, and the way to solve this problem is to add some additional features
friend
to IntWrapper
that take the left side of int
. If you try to build this example using the compiler and IntWrapper
C ++ 20 definition , you may notice that, again, it just works. Let's look at why the code above is still compiling in C ++ 20. While resolving overloads, the compiler will also collect what the standard calls “synthesized” candidates, or a rewritten expression with the reverse order of parameters. In the above example, the compiler will try to use a rewritten expression
(42 <=> a) < 0
, but will find that there is no conversion from IntWrapper
to int
to satisfy the left side, so the rewritten expression is discarded. The compiler also calls the “synthesized” expression 0 < (a <=> 42)
and discovers that the conversion from int
to occurs IntWrapper
through its conversion constructor, so this candidate is used. The purpose of synthesized expressions is to avoid confusion about the need to write function templates
friend
to fill in the gaps in which your object can be converted from other types. Synthesized expressions are generalized to 0 @ (b <=> a)
.More complex types
The spaceship operator generated by the compiler does not stop at individual members of classes, it generates the correct set of comparisons for all subobjects in your types:
structBasics {
int i;
char c;
float f;
double d;
autooperator<=>(const Basics&) const = default;
};
structArrays {
int ai[1];
char ac[2];
float af[3];
double ad[2][2];
autooperator<=>(const Arrays&) const = default;
};
structBases : Basics, Arrays {
autooperator<=>(const Bases&) const = default;
};
intmain(){
constexpr Bases a = { { 0, 'c', 1.f, 1. },
{ { 1 }, { 'a', 'b' }, { 1.f, 2.f, 3.f }, { { 1., 2. }, { 3., 4. } } } };
constexpr Bases b = { { 0, 'c', 1.f, 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);
}
The compiler knows how to expand class members, which are arrays, into their lists of subobjects and compare them recursively. Of course, if you want to write the bodies of these functions yourself, you will still benefit from rewriting expressions by the compiler.
Looks like a duck, swims like a duck, and quacks like operator==
Some very smart people on the standardization committee have noticed that the spaceship operator will always perform a lexicographic comparison of elements, no matter what. Unconditional execution of lexicographic comparisons can lead to inefficient code, in particular, with the equality operator.
A canonical example comparing two lines. If you have a string
"foobar"
and you compare it with a string "foo"
using ==, you can expect this operation to be almost constant. An effective string comparison algorithm is as follows:- First compare the size of the two lines. If the sizes are different, then return
false
- Otherwise, step through each element of two lines step by step and compare them until there is a difference or all the elements end. Return the result.
In accordance with the rules of the spaceship operator, we must start by comparing each element until we find one that is different. In our example,
"foobar"
and "foo"
only when comparing, 'b'
and '\0'
you finally return false
. To combat this, there was an article P1185R2 that details how the compiler rewrites and generates
operator==
independently of the spaceship operator. Ours IntWrapper
can be written as follows:#include<compare>structIntWrapper {
int value;
constexprIntWrapper(int value): value{value} { }
autooperator<=>(const IntWrapper&) const = default;
booloperator==(const IntWrapper&) const = default;
};
One more step ... however, there is good news; you don’t really need to write the code above, because just writing it is
auto operator<=>(const IntWrapper&) const = default
enough for the compiler to implicitly generate a separate and more efficient one operator==
for you! The compiler applies a slightly modified “rewrite” rule, specific to
==
and !=
, where in these statements they are rewritten in terms of operator==
, rather than operator<=>
. This means that it !=
also benefits from optimization.Old code won't break
At this point, you might think: well, if the compiler is allowed to perform this operator rewriting operation, what will happen if I try to outwit the compiler:
structIntWrapper {
int value;
constexprIntWrapper(int value): value{value} { }
autooperator<=>(const IntWrapper&) const = default;
booloperator<(const IntWrapper& rhs) const { return value < rhs.value; }
};
constexprboolis_lt(const IntWrapper& a, const IntWrapper& b){
return a < b;
}
The answer is no big deal. The overload resolution model in C ++ is the arena that all candidates face. In this particular battle, we have three of them:
IntWrapper::operator<(const IntWrapper& a, const IntWrapper& b)
IntWrapper::operator<=>(const IntWrapper& a, const IntWrapper& b)
(rewritten)
IntWrapper::operator<=>(const IntWrapper& b, const IntWrapper& a)
(synthesized)
If we adopted overload resolution rules in C ++ 17, the result of this call would be mixed, but the C ++ 20 overload resolution rules were changed so that the compiler could resolve this situation to the most logical overload.
There is an overload resolution phase when the compiler must complete a series of extra passes. A new mechanism has appeared in C ++ 20, within which preference is given to overloads that are not rewritten or synthesized, which makes our overload the
IntWrapper::operator<
best candidate and resolves ambiguity. The same mechanism prevents the use of synthesized candidates instead of the usual rewritten expressions.Final thoughts
The spaceship operator is a welcome addition to C ++, as it can help simplify your code and write less, and sometimes less is better. So buckle up and control your C ++ 20 spaceship !
We urge you to go out and try out the spaceship operator, it is available right now in Visual Studio 2019 under
/std:c++latest
! As a note, changes made to the P1185R2 will be available in Visual Studio 2019 version 16.2. Please keep in mind that the spaceship operator is part of C ++ 20 and subject to some changes until the time when C ++ 20 is finalized. As always, we await your feedback. Feel free to send any comments via email to visualcpp@microsoft.com, via Twitter @visualc , or Facebook Microsoft Visual Cpp .
If you encounter other problems with MSVC in VS 2019, please let us know via the “Report a Problem” option , either from the installer or from the Visual Studio IDE itself. For suggestions or bug reports, write to us through DevComm.