The Spaceship Operator <=>
(And Comparison In General)¶
Problem: Comparison Operators¶
Each type should either overload …
All 6:
==
,!=
,<
,>
,<=
,>=
(In)equality:
==
,!=
None
This much writing!
Shortcut: only
<
for occasional sorted container insertionThough containers generally support external ordering
But: incomplete overloading (e.g. omit
!=
) is bad codeWith most types, ordering is natural
Open question remain pre C++20
Strong vs. weak ordering?
When two objects of a type compare equal, can they be different?
…
Annoying: Occasional Container Insertion ⟶ operator<()
¶
Manually define
operator<()
#include <set>
#include <cassert>
struct key
{
unsigned id;
};
int main()
{
std::set<key> my_keys;
auto [_, inserted] = my_keys.insert({1}); // <-- error: no match for ‘operator<’
assert(inserted);
return 0;
}
⟶ annoying and error-prone
#include <set>
#include <cassert>
struct key
{
unsigned id;
bool operator<(const key& rhs) const
{
return id < rhs.id; // <-- annoying (and error-prone)
}
};
int main()
{
std::set<key> my_keys;
auto [_, inserted] = my_keys.insert({1});
assert(inserted);
return 0;
}
Annoying: Implementing All Six (<
, >
, <=
, >=
, ==
, !=
)¶
operator<()
is rarely enoughUser expectation: if objects are less-comparable, they should provide the full set
⟶ Implement the other five in terms of
<
⟶ Very annoying
#include <cassert>
struct key
{
unsigned id;
bool operator< (const key& rhs) const
{
return id < rhs.id;
}
bool operator==(const key& rhs) const { return !(*this < rhs) && !(rhs < *this); }
bool operator!=(const key& rhs) const { return !(*this == rhs); }
bool operator<=(const key& rhs) const { return *this < rhs || *this == rhs; }
bool operator>=(const key& rhs) const { return !(*this < rhs); }
bool operator> (const key& rhs) const { return rhs < *this && *this != rhs; }
};
int main()
{
key k1{42};
key k2{666};
assert(k1 < k2);
assert(k1 == k1);
assert(k1 != k2);
assert(k1 <= k1);
assert(k1 <= k2);
assert(k2 >= k2);
assert(k2 >= k1);
assert(k2 > k1);
return 0;
}
Annoying: Even Simple (In)Equality Comparison¶
Two-dimensional point
Expectation:
==
,!=
Even that is annoying
#include <cassert>
struct point
{
int x, y;
bool operator==(const point& rhs) const
{
return x == rhs.x && y == rhs.y;
}
bool operator!=(const point& rhs) const { return !operator==(rhs); }
};
int main()
{
point p1{1,2};
point p2{3,4};
assert(p1 == p1);
assert(p1 != p2);
return 0;
}
C++20 To The Rescue: (In)Equality¶
Rationale
C++ has always generated memberwise copy-constructor and assignment operator
Why not do something similar with (in)equality
==
and!=
Solution
==
implemented, or simply requested= default
⟶
!=
comes for free as!(a==b)
#include <cassert>
struct point
{
int x, y;
bool operator==(const point& rhs) const = default; // <-- cool!
};
struct compatible_point
{
int x, y;
operator point() const
{
return {x,y};
}
};
int main()
{
point p{1,2};
compatible_point cp{1,2};
assert(p == cp); // <-- p.operator==(cp), ok
assert(cp == p); // <-- cp.operator==(p), does not exist
return 0;
}
Pythonicity: Comparing Compatible Types¶
Member
operator==(const compatible_point& cp)
convertscp
topoint
But not
this
, as incp == p
Pythonic goodie: reverse parameters, and try again. (This is Python’s
__eq__()
method, see here)
#include <cassert>
struct point
{
int x, y;
bool operator==(const point& rhs) const = default;
};
struct compatible_point
{
int x, y;
operator point() const { return point{x,y}; }
};
int main()
{
point p{1,2};
compatible_point cp{1,2};
assert(p == cp); // <-- rhs converted to point, old-style
assert(cp == p); // <-- magic: lhs and rhs reversed
return 0;
}
Every ==
Has Its !=
¶
A type may define multiple
operator==()
(e.g. to compare with unrelated types)⟶ each has their
operator!=()
automatically… with the same Pythonic semantics just described (reversing parameters until it sees fit)
#include <cassert>
struct key
{
unsigned id;
bool operator==(unsigned rhs) const
{
return id == rhs;
}
};
int main()
{
key k{42};
assert(k == 42); // <-- explicitly defined in type
assert(k != 666); // <-- automatic !operator==()
assert(42 == k); // <-- freaky
assert(666 != k); // <-- freaky
return 0;
}
Spaceship Operator <=>
¶
a==b
(e.g.) defines a relationshipa<b
defines another relationship…
All definitions should collaborate to implement one bigger relationship
⟶ Ordering
operator<=>()
return the entire relationship==
,!=
,<
,>
,<=
,>=
are then automatically derivedstrcmp()
on steroids
Ordering Relationships¶
operator<=>()
returns one of the following ordering types:
std::strong_ordering
: implies that the values are indistinguishable (i.e. ifa == b
, thenf(a) == f(b)
).std::weak_ordering
: allows equal, distinguishable values, but doesn’t permit uncomparable values.std::partial_ordering
: allows uncomparable values (i.e.a < b
,a > b
anda == b
all return false).
To me, std::strong_ordering
seems like the one preferred
relationship to aim for.
Straightforward operator<=>()
¶
unsigned
implementsstd::strong_ordering
⟶ can be used in
= default
spaceship of surrounding type
#include <compare>
#include <cassert>
struct key
{
unsigned id;
std::strong_ordering operator<=>(const key& rhs) const = default;
};
int main()
{
key k1{42};
key k2{666};
assert(k1 < k2);
assert(k1 == k1);
assert(k1 != k2);
assert(k1 <= k1);
assert(k1 <= k2);
assert(k2 >= k2);
assert(k2 >= k1);
assert(k2 > k1);
return 0;
}
Compound Datatypes (Handwritten)¶
Typical problem: comparable type has multiple members
⟶ E.g. composite primary key in databases
Manual implementation follows:
primary
first, thensecondary
(All the five others ⟶ see here)
#include <compare>
#include <cassert>
struct key
{
unsigned primary;
unsigned secondary;
bool operator<(const key& rhs) const
{
if (primary != rhs.primary)
return primary < rhs.primary;
else
return secondary < rhs.secondary;
}
};
int main()
{
key k1{42, 1};
key k2{42, 2};
key k3{666, 1};
assert(k1 < k2);
assert(k1 < k3);
return 0;
}
Compound Datatypes: = default
Spaceship¶
= default
: applies requested ordering to members, in declaration order
#include <compare>
#include <cassert>
struct key
{
unsigned primary;
unsigned secondary;
std::strong_ordering operator<=>(const key& rhs) const = default;
};
int main()
{
key k1{42, 1};
key k2{42, 2};
assert(k1 < k2);
assert(k1 == k1);
assert(k1 != k2);
assert(k1 <= k1);
assert(k1 <= k2);
assert(k2 >= k2);
assert(k2 >= k1);
assert(k2 > k1);
return 0;
}
Some Types Have A Weaker Ordering Than std::strong_ordering
¶
double
only supportsstd::partial_ordering
NaN
does not compare well==
comparison is not so well defined tooMany other types too
#include <compare>
#include <cassert>
struct key
{
unsigned primary;
double secondary; // <-- no std::string_ordering
std::strong_ordering operator<=>(const key& rhs) const = default;
};
int main()
{
key k1{42, 1};
key k2{42, 2};
assert(k1 < k2); // <-- error: three-way comparison of ‘key::secondary’ has type ‘std::partial_ordering’, which does not convert to ‘std::strong_ordering’
return 0;
}
Pointer Members: std::strong_ordering
, But Unusable¶
secondary
isconst char*
Has
std::strong_ordering
, but compares pointer values= default
not correct
Fiasco (see P1185R2)
Implement
<=>
. This does NOT create==
(and!=
)!Implement
==
#include <compare>
#include <cstring>
#include <cassert>
struct key
{
unsigned primary;
const char* secondary;
std::strong_ordering operator<=>(const key& rhs) const
{
if (std::strong_ordering cmp = primary <=> rhs.primary; cmp != std::strong_ordering::equal)
return cmp;
// .primary are equal -> fallback to strcmp .secondary
int cmp1 = std::strcmp(secondary, rhs.secondary);
if (cmp1 < 0)
return std::strong_ordering::less;
else if (cmp1 == 0)
return std::strong_ordering::equal;
else
return std::strong_ordering::greater;
}
bool operator==(const key& rhs) const
{
return !(*this < rhs) && !(*this > rhs);
}
};
int main()
{
key k1{42, "a"};
key k2{42, "b"};
key k3{43, "b"};
assert(k1 < k2);
assert(k1 == k1);
assert(k1 != k2);
assert(k1 <= k1);
assert(k1 <= k2);
assert(k2 >= k2);
assert(k2 >= k1);
assert(k2 > k1);
assert(k1 < k3);
assert(k2 < k3);
return 0;
}
Links¶
CppCon 2019: Jonathan Müller: Using C++20’s Three-way Comparison
This is pre-standard, but really good. He did not know P1185R2 yet, though.
C++ Insights - Episode 12: C++20 The Spaceship Operator (Andreas Fertig)