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)convertscptopointBut not
this, as incp == pPythonic 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<bdefines 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 > banda == ball return false).
To me, std::strong_ordering seems like the one preferred
relationship to aim for.
Straightforward operator<=>()#
unsignedimplementsstd::strong_ordering⟶ can be used in
= defaultspaceship 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:
primaryfirst, 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#
doubleonly supportsstd::partial_orderingNaNdoes 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#
secondaryisconst char*Has
std::strong_ordering, but compares pointer values= defaultnot 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)