C++20 introduces rewriting rules that enable defining only a few operator overloads in a class to be able to compare class instances in many
ways:
- the "spaceship"
operator<=>
can replace all the other comparison operators in most cases: The code a @ b
(where
@
is one of the following operators: <
, <=
, >
, or >=
) can be implicitly
rewritten to use either a<=>b
or b<=>a
, and its three-way comparison semantics instead.
- If
operator==
is defined, a!=b
can be implicitly rewritten !(a==b)
- If an
operator<=>
is defined as =default
, a matching operator==
is automatically generated if it
does not already exist.
If you define your own version of any particular comparison operator, e.g., operator<
in addition to the
operator<=>
, it will supersede the compiler-generated version and might result in a surprising behavior with
operator<
semantics inconsistent with the semantics of other operators defined through operator<=>
.
In most cases, you will only have to define the following set of comparison operators in your class (possibly several of those sets, to allow for
mixed-type comparison):
- No comparison operator, if the class should not be compared, or
- only
operator==
for classes that can only be compared for equality (and inequality), or
- only
operator<=>
, defined as =default
for fully comparable classes that only need to perform comparison member
by member, or
- both
operator<=>
and operator==
when the comparison is more complex.
This rule will raise an issue when a class is defined:
- With an
operator<=>
and any of the four operators <
, <=
, >
,
>=
defined with the same argument type.
- With both
operator==
and operator!=
defined for the same types.
- With a defaulted
operator<=>
and a defaulted operator==
with the same argument types defined.
- With two
operator<=>
or two operator==
that are declared with the same argument types in reverse order.
Noncompliant code example
Example with redundant operations in the same class:
class A {
int field;
public:
auto operator<=>(const A&) const = default;
bool operator<(const A& other) const { // Noncompliant: this definition is redundant when operator<=> is present
return field < other.field;
}
bool operator==(const A&) const = default; // Noncompliant: unnecessary, this line is added implicitly
};
Example with equivalent operations in different order:
class MyStr {
friend std::strong_ordering operator<=>(MyStr const &s1, std::string const &s2);
friend std::strong_ordering operator<=>(std::string const &s1, MyStr const &s2); // Noncompliant, redundant with the previous line
};
Compliant solution
The class has been reduced to a minimal set:
class A {
int field;
public:
auto operator<=>(const A&) const = default; // Compliant: operator== is implicitly generated, and expressions with < can be written
};
// The following code is valid:
void f(A const &a1, A const &a2) {
bool b1 = a1 == a2; // Uses implicitly generated operator==
bool b2 = a1 != a2; // Uses implicitly generated operator==, rewritten as: !(a1 == a2)
bool b3 = a1 < a2; // Rewritten as: (a1 <=> a2) < 0
bool b4 = a1 >= a2; // Uses implicitly generated operator==
bool b1 = a1 == a2; // Uses implicitly generated operator==
}
Only one order needs to be written
class MyStr {
friend std::strong_ordering operator<=>(MyStr const &s1, std::string const &s2); // Compliant
};
// The following code is valid
void f(MyStr const &s1, std::string const &s2) {
bool b1 = s1 < s2; // Rewritten as: (s1<=>s2) < 0
bool b2 = s2 >= s1; // Rewritten as 0 >= (s1<=>s2);
}