When do you actually use <=>?

Document #: P1186R3
Date: 2019-07-16
Project: Programming Language C++
CWG
Reply-to: Barry Revzin
<>

1 Revision History

[P1186R0] was approved by both EWG and LEWG. Under Core review, the issue of unintentional comparison category strengthening was brought up as a reason to strongly oppose the design.

[P1186R1] proposed a new design to solve the issues from R0 and was presented to EWG in Kona. It was approved with a modification that synthesis of weak_ordering is only done by using both == and <. The previous versions of this proposal would try to fall-back to invoking < twice.

This paper instead of considering well-formedness of expressions or validity of expressions instead uses the notion of “shallowly well-formed” as described in [P1630R0] and is based on a new term usable function. Some examples have changed meaning as a result.

The library portion of R0 was moved into [P1188R0]. This paper is solely a proposal for language change.

2 Motivation

[P0515R3] introduced operator<=> as a way of generating all six comparison operators from a single function. As a result of [P1185R2], that has become two functions, but importantly you still only need to declare one operator function to generate each of the four relational comparison operators.

In a future world, where all types have adopted <=>, this will work great. It will be very easy to implement <=> for a type like optional<T> (writing as a non-member function for clarity):

template <typename T>
compare_3way_type_t<T> // see P1188
operator<=>(optional<T> const& lhs, optional<T> const& rhs)
{
    if (lhs.has_value() && rhs.has_value()) {
        return *lhs <=> *rhs;
    } else {
        return lhs.has_value() <=> rhs.has_value();
    }
}

This is a clean and elegant way of implementing this functionality, and gives us <, >, <=, and >= that all do the right thing. What about vector<T>?

template <typename T>
compare_3way_type_t<T>
operator<=>(vector<T> const& lhs, vector<T> const& rhs)
{
    return lexicographical_compare_3way(
        lhs.begin(), lhs.end(),
        rhs.begin(), rhs.end());
}

Even better.

What about a simple aggregate type, where all we want is to do normal member-by-member lexicographical comparison? No problem:

struct Aggr {
    X x;
    Y y;
    Z z;
    
    auto operator<=>(Aggr const&) const = default;
};

Beautiful.

2.1 An Adoption Story

The problem is that we’re not in this future world quite yet. No program-defined types have <=>, the only standard library type that has <=> so far is nullptr_t. Which means we can’t just replace the existing relational operators from optional<T> and vector<T> with <=> and probably won’t be able to just default Aggr’s <=>. We need to do something more involved.

How do we implement <=> for a type that looks like this:

// not in our immedate control
struct Legacy {
    bool operator==(Legacy const&) const;
    bool operator<(Legacy const&) const;
};

// trying to write/update this type
struct Aggr {
    int i;
    char c;
    Legacy q;
    
    // ok, easy, thanks to P1185
    bool operator==(Aggr const&) const = default;
    
    // ... but not this
    auto operator<=>(Aggr const&) const = default;
};

The implementation of <=> won’t work for Aggr. Legacy doesn’t have a <=>, so our spaceship operator ends up being defined as deleted. We don’t get the “free” memberwise comparison from just defaulting. Right now, we have to write it by hand:

strong_ordering operator<=>(Aggr const& rhs) const
{
    if (auto cmp = i <=> rhs.i; cmp != 0) return cmp;
    if (auto cmp = c <=> rhs.c; cmp != 0) return cmp;
    
    if (q == rhs.q) return strong_ordering::equal;
    if (q < rhs.q) return strong_ordering::less;
    return strong_ordering::greater;
}

Such an implementation would always give us a correct answer, but it’s not actually a good implementation. At some point, Legacy is going to adopt <=> and we really need to plan in advance for that scenario; we definitely want to use <=> whenever it’s available.

It would be better to write:

strong_ordering operator<=>(Aggr const& rhs) const
{
    if (auto cmp = i <=> rhs.i; cmp != 0) return cmp;
    if (auto cmp = c <=> rhs.c; cmp != 0) return cmp;
    return compare_3way(q, rhs.q);
}

It’s at this point that R0 went onto suggest that because compare_3way() is transparent to <=>, you may as well just always use compare_3way() and then you may as well just define <=> to be that exact logic. That language change would allow us to just = default the spaceship operator for types like Aggr.

// P1186R0, this involves just synthesizing an <=> for Legacy
auto operator<=>(Aggr const&) const = default;

2.2 The Case Against Automatic Synthesis

Consider the following legacy type:

struct Q {
    float f;
    bool operator==(Q rhs) const { return f == rhs.f; }
    bool operator<(Q rhs) const { return f < rhs.f; }
    bool operator>(Q rhs) const { return f > rhs.f; }
};

Using float just makes for a short example, but the salient point here is that Q’s ordering is partial, not total. The significance of partial orders is that these can all be false:

Q{1.0f} == Q{NAN}; // false
Q{1.0f} < Q{NAN};  // false
Q{1.0f} > Q{NAN};  // false

However, the proposed synthesis rules in P1186R0 would have led (with no source code changes!) to the following:

Q{1.0f} > Q{NAN};       // false
Q{1.0f} <=> Q{NAN} > 0; // true

This is because the proposed rules assumed a total order, wherein !(a == b) && !(a < b) imply a > b.

Now, you might ask… why don’t we just synthesize a partial ordering instead of a total ordering? Wouldn’t we get it correct in that situation? Yes, we would. But synthesizing a partial order requires an extra comparison:

friend partial_ordering operator<=>(Q const& a, Q const& b)
{
    if (a == b) return partial_ordering::equivalent;
    if (a < b)  return partial_ordering::less;
    if (b < a)  return partial_ordering::greater;
    return partial_ordering::unordered;
}

Many types which do not provide <=> do still implement a total order. While assuming a partial order is completely safe and correct (we might say equivalent when it really should be equal, but at least we won’t ever say greater when it really should be unordered!), for many types that’s a performance burden. For totally ordered types, that last comparison is unnecessary - since by definition there is no case where we return unordered. It would be unfortunate to adopt a language feature as purely a convenience feature to ease adoption of <=>, but end up with a feature that many will eschew and hand-write their own comparisons - possibly incorrectly.

The goal of this proposal is to try to have our cake an eat it too:

The first bullet implies the need for some language change. The second bullet kills P1186R0, the third bullet kills a variant of P1186R0 that would synthesize partial_ordering instead of strong_ordering, and the two taken together basically ensure that we cannot have a language feature that synthesis <=> for a type with opt-in.

2.3 An Adoption Story for Templates

Taking a step to the side to talk about an adoption story for class templates. How would vector<T> and optional<T> and similar containers and templates adopt operator<=>?

R0 of this paper argued against the claim that “[a]ny compound type should have <=> only if all of its constituents have <=>.” At the time, my understanding of what “conditional spaceship” meant was this:

// to handle legacy types. This is called Cpp17LessThanComparable in the
// working draft
template <typename T>
concept HasLess = requires (remove_reference_t<T> const& t) {
    { t < t } -> bool
};

template <HasLess T>
bool operator<(vector<T> const&, vector<T> const&);

template <ThreeWayComparable T> // see P1188
compare_3way_type_t operator<=>(vector<T> const&, vector<T> const&);

This is, indeed, a bad implementation strategy because v1 < v2 would invoke operator< even if operator<=> was a viable option, so we lose the potential performance benefit. It’s quite important to ensure that we use <=> if that’s at all an option. It’s this problem that partially led to my writing P1186R0.

But since I wrote this paper, I’ve come up with a much better way of conditionally adopting spaceship [Revzin]:

template <HasLess T>
bool operator<(vector<T> const&, vector<T> const&);

template <ThreeWayComparable T> requires HasLess<T>
compare_3way_type_t operator<=>(vector<T> const&, vector<T> const&);

It’s a small, seemingly redundant change (after all, if ThreeWayComparable<T> then surely HasLess<T> for all types other than pathologically absurd ones that provide <=> but explicitly delete <), but it ensures that v1 < v2 invokes operator<=> where possible.

Conditionally adopting spaceship between C++17 and C++20 is actually even easier:

template <typename T>
enable_if_t<supports_lt<T>::value, bool> // normal C++17 SFINAE machinery
operator<(vector<T> const&, vector<T> const&);

// use the feature-test macro for operator<=>
#if __cpp_impl_three_way_comparison
template <ThreeWayComparable T>
compare_3way_type_t<T> operator<=>(vector<T> const&, vector<T> const&);
#endif    

In short, conditionally adopting <=> has a good user story, once you know how to do it. This is very doable, and is no longer, if of itself, a motivation for making a language change. It is, however, a motivation for not synthesizing <=> in a way that leads to incorrect answers or poor performance - as this would have far-reaching effects.

The above is solely about the case where we want to adopt <=> conditionally. If we want to adopt <=> unconditionally, we’ll need to do the same kind of things in the template case as we want to do in the non-template case. We need some way of invoking <=> where possible, but falling back to a synthesized three-way comparison from the two-way comparison operators.

2.4 Status Quo

To be perfectly clear, the current rule for defaulting operator<=> for a class C is roughly as follows:

In other words, for the Aggr example, the declaration strong_ordering operator<=>(Aggr const&) const = default; expands into something like

struct Aggr {
    int i;
    char c;
    Legacy q;
    
    strong_ordering operator<=>(Aggr const& rhs) const {
        if (auto cmp = i <=> rhs.i; cmp != 0) return cmp;
        if (auto cmp = c <=> rhs.c; cmp != 0) return cmp;
        if (auto cmp = q <=> rhs.q; cmp != 0) return cmp; // (*)
        return strong_ordering::equal
    }
};

Or it would, if the marked line were valid. Legacy has no <=>, so that pairwise comparison is ill-formed, so the operator function would be defined as deleted.

3 Proposal

This paper proposes a new direction for a stop-gap adoption measure for operator<=>: we will synthesize an operator<=> for a type, but only under very specific conditions, and only when the user provides the comparison category that the comparison needs to use. All we need is a very narrow ability to help with <=> adoption. This is that narrow ability.

Currently, the pairwise comparison of the subobjects is always xi <=> yi. Always operator<=>.

This paper proposes defining a new say of synthesizing a three-way comparison, which only has meaning in the context of defining what a defaulted operator<=> does. The function definition is very wordy, but it’s not actually complicated: we will use the provided return type to synthesize an appropriate ordering. The key points are:

We then change the meaning of defaulted operator<=> to be defined in terms of this new synthesis instead of in terms of xi <=> yi.

3.1 Soundness of Synthesis

It would be sound to synthesize strong_ordering from just performing < both ways, but equality is the salient difference between weak_ordering and strong_ordering and it doesn’t seem right to synthesize a strong_ordering from a type that doesn’t even provide an ==.

There is no other sound way to synthesize partial_ordering from == and <. If we just do < both ways, we’d have to decide between equivalent and unordered in the case where !(a < b) && !(b < a) - the former gets the unordered cases wrong and the latter means our order isn’t reflexive.

3.2 What does it mean to require an operation

In the above description, I said that a synthesis might require both == and <. What does that actually mean? How much do we want to require? At the very extreme end, we might require both a == b and a < b be well-formed expressions whose types are bool. But we cannot require that the expression is well-formed, the most we can do is say that overload resolution succeeds and finds a candidate that isn’t deleted or inaccessible. Do we want to require contextually convertible to bool? Exactly bool?

Ultimately, the questions are all about: what do we want to happen in the error cases? Do we want to end up with a deleted spaceship or an ill-formed spaceship?

Consider these two cases:

struct Eq {
    friend bool operator==(Eq, Eq);
};

struct Weak {
    friend weak_ordering operator<=>(Weak, Weak);
};

struct SomeDsl {
    struct NotBool { };
    NotBool operator==(SomeDsl) const;
    NotBool operator<(SomeDsl) const;
};

struct Nothing { };

template <typename T>
struct C {
    T t;
    strong_ordering operator<=>(C const&) const = default;
};

Clearly none of C<Eq>, C<Weak>, C<SomeDsl>, and C<Nothing> can have a valid operator<=>, but should they be deleted or ill-formed? Erring on the side of ill-formed helps programmers catch bugs earlier. Erring on the side of deleted lets you actually check programmaticaly if a type has a function or not. A defaulted copy constructor, for instance, is defined as deleted if some member isn’t copyable - it isn’t ill-formed. Arguably comparison is a very nearly a special member function.

To that end, I propose we split the difference here. This particular proposal is entirely about being a stop-gap for types that don’t have spaceship - it really shouldn’t be broadly used in templates. The line I’m going to draw is that we check that the functions we need exist and are usable but we don’t check that their return types meet our requirements. In other words:

I think that’s the right line.

3.3 Explanatory Examples

This might make more sense with examples.

Source Code
Meaning
struct Aggr {
  int i;
  char c;
  Legacy q;

  auto operator<=>(Aggr const&) const
        = default;
};
struct Aggr {
  int i;
  char c;
  Legacy q;
    
  // x.q <=> y.q does not find a usable function
  // and we have no return type to guide our
  // synthesies. Hence, deleted.
  auto operator<=>(Aggr const&) const
        = delete;
};
struct Aggr {
  int i;
  char c;
  Legacy q;
    
  strong_ordering operator<=>(Aggr const&) const
        = default;
};
struct Aggr {
  int i;
  char c;
  Legacy q;
    
  strong_ordering operator<=>(Aggr const& rhs) const {
    // pairwise <=> works fine for these
    if (auto cmp = i <=> rhs.i; cmp != 0) return cmp;
    if (auto cmp = c <=> rhs.c; cmp != 0) return cmp;
    
    // synthesizing strong_ordering from == and <
    if (q == rhs.q) return strong_ordering::equal;
    if (q < rhs.q) return strong_ordering::less;
    
    // sanitizers might also check for
    [[ assert: rhs.q < q; ]]
    return strong_ordering::greater;
  }
};
struct X {
  bool operator<(X const&) const;
};

struct Y {
  X x;
  
  strong_ordering operator<=>(Y const&) const
        = default;
};
struct X {
  bool operator<(X const&) const;
};

struct Y {
  X x;
  
  // defined as deleted because X has no <=>,
  // so we fallback to synthesizing from ==
  // and <, but we have no ==.
  strong_ordering operator<=>(Y const&) const
        = delete;
};
struct W {
  weak_ordering operator<=>(W const&) const;
};

struct Z {
  W w;
  Legacy q;
  
  strong_ordering operator<=>(Z const&) const
        = default;
};
struct W {
  weak_ordering operator<=>(W const&) const;
};

struct Z {
  W w;
  Legacy q;
  
  strong_ordering operator<=>(Z const& rhs) const {
    // W has a <=>, but its return type is not
    // convertible to strong_ordering. So this
    // operator is simply ill-formed. Instantiating
    // it is a hard error    
    if (auto cmp = static_cast<strong_ordering>(
            w <=> rhs.w); cmp != 0) return cmp;
        
    if (q == rhs.q) return strong_ordering::equal;
    if (q < rhs.q) return strong_ordering::less;
    return strong_ordering::equal;
  }
};
struct W {
  weak_ordering operator<=>(W const&) const;
};

struct Q {
  bool operator==(Q const&) const;
  bool operator<(Q const&) const;
};

struct Z {
  W w;
  Q q;
  
  weak_ordering operator<=>(Z const&) const 
        = default;
};
struct W {
  weak_ordering operator<=>(W const&) const;
};

struct Q {
  bool operator==(Q const&) const;
  bool operator<(Q const&) const;
};

struct Z {
  W w;
  Q q;
  
  weak_ordering operator<=>(Z const& rhs) const
  {
    if (auto cmp = w <=> rhs.w; cmp != 0) return cmp;
    
    // synthesizing weak_ordering from == and <
    if (q == rhs.q) return weak_ordering::equivalent;
    if (q < rhs.q)  return weak_ordering::less;
    return weak_ordering::greater;
  }
};

3.4 Differences from Status Quo and P1186R0

Consider the highlighted lines in the following example:

struct Q {
    bool operator==(Q const&) const;
    bool operator<(Q const&) const;
};

Q{} <=> Q{}; // #1

struct X {
    Q q;
    auto operator<=>(X const&) const = default; // #2
};

struct Y {
    Q q;
    strong_ordering operator<=>(Y const&) const = default; // #3
};

In the working draft, #1 is ill-formed and #2 and #3 are both defined as deleted because Q has no <=>.

With P1186R0, #1 is a valid expression of type std::strong_ordering, and #2 and #3 are both defined as defaulted. In all cases, synthesizing a strong comparison.

With this proposal, #1 is still ill-formed. #2 is defined as deleted, because Q still has no <=>. The only change is that in the case of #3, because we know the user wants strong_ordering, we provide one.

3.5 Building complexity

The proposal here only applies to the specific case where we are defaulting operator<=> and provide the comparison category that we want to default to. That might seem inherently limiting, but we can build up quite a lot from there.

Consider std::pair<T, U>. Today, its operator<= is defined in terms of its operator<, which assumes a weak ordering. One thing we could do (which this paper is not proposing, this is just a thought experiment) is to synthesize <=> with weak ordering as a fallback.

We do that with just a simple helper trait (which this paper is also not proposing):

// use whatever <=> does, or pick weak_ordering
template <typename T, typename C>
using fallback_to = conditional_t<ThreeWayComparable<T>, compare_3way_type_t<T>, C>;

// and then we can just...
template <typename T, typename U>
struct pair {
    T first;
    U second;
    
    common_comparison_category_t<
        fallback_to<T, weak_ordering>,
        fallback_to<U, weak_ordering>>
    operator<=>(pair const&) const = default;
};

pair<T,U> is a simple type, we just want the default comparisons. Being able to default spaceship is precisely what we want. This proposal gets us there, with minimal acrobatics. Note that as a result of P1185R0, this would also give us a defaulted ==, and hence we get all six comparison functions in one go.

Building on this idea, we can create a wrapper type which defaults <=> using these language rules for a single type, and wrap that into more complex function objects:

// a type that defaults a 3-way comparison for T for the given category
template <typename T, typename Cat>
struct cmp_with_fallback {
    T const& t;
    fallback_to<T,Cat> operator<=>(cmp_with_fallback const&) const = default;
};

// Check if that wrapper type has a non-deleted <=>, whether because T
// has one or because T provides the necessary operators for one to be
// synthesized per this proposal
template <typename T, typename Cat>
concept FallbackThreeWayComparable =
    ThreeWayComparable<cmp_with_fallback<T, Cat>>;

// Function objects to do a three-way comparison with the specified fallback
template <typename Cat>
struct compare_3way_fallback_t {
    template <FallbackThreeWayComparable<Cat> T>
    constexpr auto operator()(T const& lhs, T const& rhs) {
        using C = cmp_with_fallback<T, Cat>;
        return C{lhs} <=> C{rhs};
    }
};

template <typename Cat>
inline constexpr compare_3way_fallback_t<Cat> compare_3way_fallback{};

And now implementing <=> for vector<T> unconditionally is straightforward:

template <FallbackThreeWayComparable<weak_ordering> T>
constexpr auto operator<=>(vector<T> const& lhs, vector<T> const& rhs) {
    // Use <=> if T has it, otherwise use a combination of either ==/<
    // or just < based on what T actually has. The proposed language
    // change does the right thing for us
    return lexicographical_compare_3way(
        lhs.begin(), lhs.end(),
        rhs.begin(), rhs.end(),
        compare_3way_fallback<weak_ordering>);
}

As currently specified, std::weak_order() and std::partial_order() from [cmp.alg] basically follow the language rules proposed here. We can implement those with a slightly different approach to the above - no fallback necessary here because we need to enforce a particular category:

template <typename T, typename Cat>
struct compare_as {
    T const& t;
    Cat operator<=>(compare_as const&) const = default;
};

// Check if the compare_as wrapper has non-deleted <=>, whether because T
// provides the desired comparison category or because we can synthesize one
template <typename T, typename Cat>
concept SyntheticThreeWayComparable = ThreeWayComparable<compare_as<T, Cat>, Cat>;

template <SyntheticThreeWayComparable<weak_ordering> T>
weak_ordering weak_order(T const& a, T const& b) {
    using C = compare_as<T, weak_ordering>;
    return C{a} <=> C{b};
}

template <SyntheticThreeWayComparable<partial_ordering> T>
partial_ordering partial_order(T const& a, T const& b) {
    using C = compare_as<T, partial_ordering>;
    return C{a} <=> C{b};
}

None of the above is being proposed, it’s just a demonstration that this language feature is sufficient to build up fairly complex tools in a short amount of code.

3.6 What about compare_3way()?

Notably absent from this paper has been a real discussion over the fate of std::compare_3way(). R0 of this paper made this algorithm obsolete, but that’s technically no longer true. It does, however, fall out from the tools we will need to build up in code to solve other problems. In fact, we’ve already written it:

constexpr inline auto compare_3way = compare_3way_fallback<strong_ordering>;

For further discussion, see [P1188R0]. This paper focuses just on the language change for operator<=>.

3.7 What about XXX_equality?

This paper proposes synthesizing strong_equality and weak_equality orderings, simply for consistency, even if such return types from operator<=> are somewhat questionable. As long as we have language types for which <=> yields a comparison category of type XXX_equality, all the rules we build on top of <=> should respect that and be consistent.

4 Wording

[ Editor's note: The wording here introduces the term usable function, which is also introduced in [P1630R0] with the same wording. ]

Add a new subbullet in 6.2 [basic.def.odr], paragraph 12:

12 Given such an entity named D defined in more than one translation unit, then

  • (12.8) if D is a class with a defaulted three-way comparison operator function ([class.spaceship]), it is as if the operator was implicitly defined in every translation unit where it is odr-used, and the implicit definition in every translation unit shall call the same comparison operators for each subobject of D.

Insert a new paragraph before 11.10.3 [class.spaceship], paragraph 1:

0 The synthesized three-way comparison for comparison category type R ([cmp.categories]) of glvalues a and b of the same type is defined as follows:

  • (0.1) If overload resolution for a <=> b finds a usable function ([over.match]), static_cast<R>(a <=> b);

  • (0.2) Otherwise, if overload resolution for a <=> b finds at least one viable candidate, the synthesized three-way comparison is not defined;

  • (0.3) Otherwise, if R is strong_ordering, then

    a == b ? strong_ordering::equal : 
    a < b  ? strong_ordering::less : 
             strong_ordering::greater
  • (0.4) Otherwise, if R is weak_ordering, then

    a == b ? weak_ordering::equal : 
    a < b  ? weak_ordering::less : 
             weak_ordering::greater
  • (0.5) Otherwise, if R is partial_ordering, then

    a == b ? partial_ordering::equivalent : 
    a < b  ? partial_ordering::less :
    b < a  ? partial_ordering::greater :
             partial_ordering::unordered
  • (0.6) Otherwise, if R is strong_equality, then a == b ? strong_equality::equal : strong_equality::nonequal;

  • (0.7) Otherwise, if R is weak_equality, then a == b ? weak_equality::equivalent : weak_equality::nonequivalent;

  • (0.8) Otherwise, the synthesized three-way comparison is not defined.

[Note: A synthesized three-way comparison may be ill-formed if overload resolution finds usable functions that do not otherwise meet the requirements implied by the defined expression. -end node ]

Change 11.10.3 [class.spaceship], paragraph 1:

1 Given an expanded list of subobjects for an object x of type C, the type of the expression xi <=> xi is denoted by Ri. If overload resolution as applied to xi <=> xi does not find a usable function, then Ri is void. If the declared return type of a defaulted three-way comparison operator function is auto, then the return type is deduced as the common comparison type (see below) of R0, R1, …, Rn−1. [Note: Otherwise, the program will be ill-formed if the expression xi <=> xi is not implicitly convertible to the declared return type for any i.—end note] If the return type is deduced as void, the operator function is defined as deleted. If the declared return type of a defaulted three-way comparison operator function is R and the synthesized three-way comparison for comparison category type R between any objects xi and xi is not defined or would be ill-formed, the operator function is defined as deleted.

Change 11.10.3 [class.spaceship], paragraph 2, to use the new synthesized comparison instead of <=>

2 The return value V of type R of the defaulted three-way comparison operator function with parameters x and y of the same type is determined by comparing corresponding elements xi and yi in the expanded lists of subobjects for x and y (in increasing index order) until the first index i where xi <=> yi the synthesized three-way comparison for comparison category type R between xi and yi yields a result value vi where vi != 0, contextually converted to bool, yields true; V is vi converted to R. If no such index exists, V is std::strong_ordering::equal converted to R.

Add to the end of 12.3 [over.match], the new term usable function:

Overload resolution results in a usable function if overload resolution succeeds and the selected function is not deleted and is accessible from the context in which overload resolution was performed.

Update the feature-test macro in 15.10 [cpp.predefined] for __cpp_impl_three_way_comparison to the date of the editor’s choosing:

Macro name Value
__cpp_impl_three_way_comparison 201711L ??????L

[ Editor's note: Why is it __cpp_impl_three_way_comparison as opposed to __cpp_three_way_comparison or even __cpp_spaceship? ]

5 Acknowledgments

Thanks to Gašper Ažman, Agustín Bergé, Jens Maurer, Richard Smith, Jeff Snyder, Tim Song, Herb Sutter, and Tony van Eerd for the many discussions around these issues. Thanks to the Core Working Group for being vigilant and ensuring a better proposal.

6 References

[P0515R3] Herb Sutter, Jens Maurer, Walter E. Brown. 2017. Consistent comparison.
https://wg21.link/p0515r3

[P1185R2] Barry Revzin. 2019. <=> != ==.
https://wg21.link/p1185r2

[P1186R0] Barry Revzin. 2019. When do you actually use <=>?
https://wg21.link/p1186r0

[P1186R1] Barry Revzin. 2019. When do you actually use <=>?
https://wg21.link/p1186r1

[P1188R0] Barry Revzin. 2019. Library utilities for <=>.
https://wg21.link/p1188r0

[P1630R0] Barry Revzin. 2019. Spaceship needs a tune-up: Addressing some discovered issues with P0515 and P1185.
https://wg21.link/p1630r0

[Revzin] Barry Revzin. 2018. Conditionally implementing spaceship.
https://brevzin.github.io/c++/2018/12/21/spaceship-for-vector/