What are the breaking changes caused by rewritten comparison operators?

That particular aspect is a simple form of rewriting, reversing the operands. The primary operators == and <=> can be reversed, the secondaries !=, <, >, <=, and >=, can be rewritten in terms of the primaries.

The reversing aspect can be illustrated with a relatively simple example.

If you don’t have a specific B::operator==(A) to handle b == a, you can use the reverse to do it instead: A::operator==(B). This makes sense because equality is a bi-directional relationship: (a == b) => (b == a).

Rewriting for secondary operators, on the other hand, involves using different operators. Consider a > b. If you cannot locate a function to do that directly, such as A::operator>(B), the language will go looking for things like A::operator<=>(B) then simply calculating the result from that.

That’s a simplistic view of the process but it’s one that most of my students seem to understand. If you want more details, it’s covered in the [over.match.oper] section of C++20, part of overload resolution (@ is a placeholder for the operator):

For the relational and equality operators, the rewritten candidates include all member, non-member, and built-in candidates for the operator <=> for which the rewritten expression (x <=> y) @ 0 is well-formed using that operator<=>.

For the relational, equality, and three-way comparison operators, the rewritten candidates also include a synthesized candidate, with the order of the two parameters reversed, for each member, non-member, and built-in candidate for the
operator <=> for which the rewritten expression 0 @ (y <=> x) is well-formed using that operator<=>.

Hence gone are the days of having to provide a real operator== and operator<, then boiler-plating:

operator!=      as      !  operator==
operator>       as      ! (operator== || operator<)
operator<=      as         operator== || operator<
operator>=      as      !  operator<

Don’t complain if I’ve gotten one or more of those wrong, that just illustrates my point on how much better C++20 is, since you now only have to provide a minimal set (most likely just operator<=> plus whatever else you want for efficiency) and let the compiler look after it 🙂

The question as to why one is being selected over the other can be discerned with this code:

#include <iostream>

struct B {};
struct A {
    bool operator==(B const&) { std::cout << "1\n"; return true; }
bool operator==(B const&, A const&) { std::cout << "2\n"; return true; }

int main() {
  auto b = B{}; auto a = A{};

           b ==          a;  // outputs: 1
  (const B)b ==          a;  //          1
           b == (const A)a;  //          2
  (const B)b == (const A)a;  //          2

The output of that indicates that it’s the const-ness of a deciding which is the better candidate.

As an aside, you may want to have a look at this article, which offers a more in-depth look.

Leave a Comment