History of the question
Before C++20, developers manually implemented six comparison operators for sortable types. This boilerplate frequently introduced subtle logical inconsistencies between equality and ordering relations. The spaceship operator was introduced to consolidate these into a single canonical operation.
The problem
While operator<=> reduces syntax, the compiler relies on its return type to synthesize reversed expressions like b < a from a > b. Without knowing if the ordering is strong, weak, or partial, the compiler cannot safely generate these rewrites.
The solution
The return type must be std::strong_ordering, std::weak_ordering, or std::partial_ordering (or implicitly convertible). This standard category allows the compiler to generate reversed candidates and implicit equality checks. Returning auto or custom types disables this synthesis, requiring manual asymmetric overloads.
struct Widget { int id; // Correct: enables reversed candidate generation std::strong_ordering operator<=>(const Widget&) const = default; };
Scenario and Problem
Developing a SpatialIndex for GPU-accelerated geometry required a BoundingBox struct with strict weak ordering for std::set insertion. The boxes needed to compare against raw coordinate arrays for spatial queries.
Solution 1: Manual operator overloading
Implementing twelve overloads (six for BoundingBox, six for coordinate arrays) provided explicit control. However, the verbosity risked copy-paste errors between operator< and operator>, and maintaining consistency during refactors proved tedious.
Solution 2: Defaulted spaceship returning std::weak_ordering
This generated all relational operators automatically from a single declaration. The explicit return type allowed the compiler to handle reversed comparisons against coordinate arrays. The implementation guaranteed exception safety and mathematical consistency with zero boilerplate.
Solution 3: Auto return
Using auto operator<=>(const BoundingBox&) const = default prevented reversed candidate synthesis. Comparing a raw array on the left to a BoundingBox on the right failed to compile. This asymmetry broke the spatial query interface.
Decision and Outcome
We chose Solution 2 with std::weak_ordering because bounding boxes have equivalence (intersecting boxes compare equal) but not mathematical equality. This enabled seamless integration with standard algorithms while supporting heterogeneous coordinate comparisons.
Why does the compiler synthesize operator== from operator<=>, and when is this suboptimal?
The compiler generates operator== as ((*this <=> other) == 0). This provides consistency but forces a full element-wise comparison even when checking equality. Explicitly defaulting operator== allows short-circuit evaluation, returning false immediately upon the first differing member.
How does defining operator<=> as a member rather than a hidden friend break symmetry?
A member operator<=> only permits implicit conversions on the right-hand operand during overload resolution. This asymmetry prevents expressions like double == MyClass from compiling even if MyClass is constructible from double. Using a hidden friend enables Argument Dependent Lookup (ADL), allowing both operands to convert implicitly.
What distinguishes std::compare_three_way from manual pointer comparison?
std::compare_three_way provides a total order for pointers that is consistent across the entire address space, including std::nullptr_t. Manual pointer comparisons using relational operators invoke undefined behavior when comparing unrelated objects. Using the standard function object ensures portable, well-defined semantics for pointer sorting.