Let's try C++20 | Compare objects
Tags: cpp, programming
Published: 2020-11-06 10:51:21

# Let's try C++20 | comparisons

C++20 comes with new possibilities to compare objects.

Let's start with this simple code where we try to compare 2 objects of user-defined type:

```cpp
#include <iostream>

struct Foo {
    const int value;
};

int main() {
    Foo a{42};
    Foo b{66};

    std::cout << std::boolalpha;
    std::cout << (a == b) << '\n';
    std::cout << (a != b) << '\n';

    std::cout << (a > b) << '\n';
    std::cout << (a >= b) << '\n';

    std::cout << (a < b) << '\n';
    std::cout << (a <= b) << '\n';
}
```

This code doesn't compile, no matter which version of C++ you are using. Indeed, none of these operators are implicitly defined by the compiler and we get 6 error (one per operator):

```bash
prog.cc: In function 'int main()':
prog.cc:12:21: error: no match for 'operator==' (operand types are 'Foo' and 'Foo')
   12 |     std::cout << (a == b) << '\n';
      |                   ~ ^~ ~
      |                   |    |
      |                   Foo  Foo
prog.cc:13:21: error: no match for 'operator!=' (operand types are 'Foo' and 'Foo')
   13 |     std::cout << (a != b) << '\n';
      |                   ~ ^~ ~
      |                   |    |
      |                   Foo  Foo
prog.cc:15:21: error: no match for 'operator>' (operand types are 'Foo' and 'Foo')
   15 |     std::cout << (a > b) << '\n';
      |                   ~ ^ ~
      |                   |   |
      |                   Foo Foo
prog.cc:16:21: error: no match for 'operator>=' (operand types are 'Foo' and 'Foo')
   16 |     std::cout << (a >= b) << '\n';
      |                   ~ ^~ ~
      |                   |    |
      |                   Foo  Foo
prog.cc:18:21: error: no match for 'operator<' (operand types are 'Foo' and 'Foo')
   18 |     std::cout << (a < b) << '\n';
      |                   ~ ^ ~
      |                   |   |
      |                   Foo Foo
prog.cc:19:21: error: no match for 'operator<=' (operand types are 'Foo' and 'Foo')
   19 |     std::cout << (a <= b) << '\n';
      |                   ~ ^~ ~
      |                   |    |
      |                   Foo  Foo
```

Until C++17, we had to define all operators manually. We were not even able to default them:

```cpp
struct Foo {
    const int value;

    bool operator==(const Foo& other) = default;
};
```

Compiler error was:

> `error: defaulted 'bool Foo::operator==(const Foo&)' only available with '-std=c++20' or '-std=gnu++20'`

## Defaulted `operator==()`

Let's simply listen to GCC and compile our code with `-std=c++20` option:

> `error: defaulted 'bool Foo::operator==(const Foo&)' must be 'const'`

When we add `const`, the compiler generates `operator==()` as requested and it implicitly defines `operator=!()`. In fact, the later is also implicitly defined if the former is user-defined.

Consequence: the 6 errors about missing operators are reduced to 4. `operator==()` and `operator=!()` work as expected:

```cpp
int main() {
    Foo a{42};
    Foo b{66};

    std::cout << std::boolalpha;
    std::cout << (a == b) << '\n';
    std::cout << (a != b) << '\n';
}
```

Output:

```bash
false
true
```

What does this defaulted `operator==()` do? As always, [cppreference](https://en.cppreference.com/w/cpp/language/default_comparisons) has the answer. "Compiler generates element-wise equality testing" for this operator:

> A class can define `operator==` as defaulted, with a return value of `bool`. This will generate an equality comparison of each base class and member subobject, in their declaration order. Two objects are equal if the values of their base classes and members are equal. The test will short-circuit if an inequality is found in members or base classes earlier in declaration order.

## Try to Default Other Operators

You may be tempted to default other comparison operators. Spoiler alert: it won't work (*). From the 6 operators, only `operator()==` can be defaulted.

```cpp
#include <iostream>

struct Foo {
    const int value;

    bool operator>(const Foo& other) const = default;
};

int main() {
    Foo a{42};
    Foo b{66};

    std::cout << std::boolalpha;
    std::cout << (a > b) << '\n';
}
```

Errors are:

```bash
prog.cc: In function 'int main()':
prog.cc:14:23: error: use of deleted function 'bool Foo::operator>(const Foo&) const'
   14 |     std::cout << (a > b) << '\n';
      |                       ^
prog.cc:6:10: note: 'bool Foo::operator>(const Foo&) const' is implicitly deleted because the default definition would be ill-formed:
    6 |     bool operator>(const Foo& other) const = default;
      |          ^~~~~~~~
prog.cc:6:10: error: no match for 'operator<=>' (operand types are 'const Foo' and 'const Foo')
```

This is where three-way comparison comes into play.

(*) = [cppreference seems to say that it is possible](https://en.cppreference.com/w/cpp/language/default_comparisons) to default all 6 comparison operators, but I got errors with both gcc and clang, except with `operator()==`. Seems like they can be defaulted only if `operator==` and/or `operator<=>` are defined.

## Three-way Comparison

There is a new operator in C++20: `operator<=>()`. It is called "spaceship operator" and it performs a [three-way comparison](https://en.wikipedia.org/wiki/Three-way_comparison):

> A three-way comparison takes two values A and B belonging to a type with a total order and determines whether A < B, A = B, or A > B in a single operation, in accordance with the mathematical law of trichotomy.

You may not be familiar with the names "spaceship operator" or "three-way comparison" but you have probably experienced them already.

Remember [`strcmp()` from C](https://linux.die.net/man/3/strcmp)?

> `int strcmp(const char *s1, const char *s2);`
>
> Return Value
>
> The `strcmp()` and `strncmp()` functions return an integer less than, equal to, or greater than zero if `s1` (or the first n bytes thereof) is found, respectively, to be less than, to match, or be greater than `s2`.

Ever tried the [Comparable interface in Java](https://docs.oracle.com/en/java/javase/15/docs/api/java.base/java/lang/Comparable.html)?

> Interface `Comparable<T>`
>
> `int compareTo​(T o)`
> Returns: a negative integer, zero, or a positive integer as this object is less than, equal to, or greater than the specified object.

These are three-way comparisons.

## Defaulted `operator<=>`

Let's get back to our `struct Foo` and try to default this spaceship operator. Doing so will also generate other operators:

> A class that defines `operator<=>` as defaulted will generate a 3-way comparison element-wise. It will perform 3-way comparisons on each base class and member subobject, in declaration order. These comparisons will short-circuit on the first non-equal base class or member. If the return type of the defaulted function is `auto`, then the comparison type will be the most restrictive of all of the 3-way comparison results for its subobjects.
>
> Per the rules for any `operator<=>` overload, a defaulted `<=>` overload will also allow the type to be compared with `<`, `<=`, `>`, and `>=`. If `operator<=>` is defaulted and `operator==` is not declared at all, then `operator==` is implicitly defaulted.

```cpp
#include <compare> // this is important

struct Foo {
    const int value;

    auto operator<=>(const Foo&) const = default;
};

#include <iostream>

int main() {
    Foo a{42};
    Foo b{66};

    std::cout << std::boolalpha;
    std::cout << (a == b) << '\n';
    std::cout << (a != b) << '\n';

    std::cout << (a > b) << '\n';
    std::cout << (a >= b) << '\n';

    std::cout << (a < b) << '\n';
    std::cout << (a <= b) << '\n';
}
```

This code compiles and outputs:

```bash
false
true
false
false
true
true
```

Ain't that great?! 😍

## Object Comparison Really Changes in C++20

Early papers for C++20 proposed that `operator<=>()` would be used to generate all other 6 operators, including `operator==()`. [Barry Revzin](https://brevzin.github.io/) then writes several papers to change this behavior. See for instance [P1630R1: Spaceship needs a tune-up](http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2019/p1630r1.html). Eventually, his document [P1185R2: `<=> != ==`](http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2019/p1185r2.html) has made it into the standard, as you can see in the section "Three-way comparison (“spaceship”) and defaulted comparisons" on [Changes between C++17 and C++20 DIS](http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2020/p2131r0.html).

They are many papers in this section and they create a complete change in how we will compare objects in C++.

## Primary vs Secondary Operators

The first important change is the new spaceship operator and the possibility to default comparison operators. These operators can be split in 2 groups : on one side, we have equality operators `==` and `!=`, and on the other side we have relational operators `<=>`, `>`,  `>=`, `<`, `<=`.

In it's a blog article ["Comparisons in C++20"](https://brevzin.github.io/c++/2019/07/28/comparisons-cpp20/), Barry Revzin introduces the names "primary" and "secondary" operators. `==` and `<=>` are primary, the other are secondary.

Primary operators can be defaulted. Secondary operators can be defaulted if the corresponding primary operator is defined.

Secondary operators can be implicitly defined by the compiler if the primary operators are user-defined or defaulted. For instance, defaulted `!=` generates a call to negated `==`.

Defaulting `<=>` will also implicitly default `==` if it is not defined, and hence defaulting all operators.

Defaulted `==` and `!=` do **not** invoke `<=>`.

## Reversing Comparisons

The second important change is the ability for compilers to reverse operators and operands in comparisons.

To understand this, let's get back to C++17 and consider this code:

```cpp
struct Foo {
    const int value;

    bool operator==(int v) const {
        return value == v;
    }
};

#include <iostream>

int main() {
    Foo foo{42};

    std::cout << std::boolalpha << (foo == 42);
}
```

This code compiles fine and prints `true`. Unfortunately, `std::cout << std::boolalpha << (42 == foo);` doesn't compile:

> `error: no match for 'operator==' (operand types are 'int' and 'Foo')`

The solution is to write a free operator:

```cpp
bool operator==(int v, Foo foo) {
    return foo.value == v;
}
```

In C++20, you don't have to do that. Both comparisons compile and print `true`. Indeed, primary operators can be reversed automatically by the compiler. When the compiler evaluates `42 == foo` but doesn't find a suitable operator, it reverses the operation as `foo == 42` and looks again for a suitable operator, which is found in my example.

In the [blog article linked above](https://brevzin.github.io/c++/2019/07/28/comparisons-cpp20/), Barry Revzin says:

> In C++20, expressions containing secondary comparison operators will also try to look up their corresponding primary operators and write the secondary comparison in terms of the primary.
>
> Because the language considers reversing candidates, you can write all of these operators as member functions too. No more writing non-member functions just to handle heterogeneous comparison.

Its means that secondary operators cannot be reversed on their own. For instance, the following code doesn't compile (not even in C++20):

```cpp
struct Foo {
    const int value;

    bool operator!=(int v) const {
        return value == v;
    }
};

#include <iostream>

int main() {
    Foo foo{42};

    std::cout << std::boolalpha << (42 != foo);
}
```

The error is:

> `error: no match for 'operator!=' (operand types are 'int' and 'Foo')`

Nevertheless, the compiler is able to reverse an operation with a secondary operator by using the corresponding primary operator. Here is a correct code:

```cpp
struct Foo {
    const int value;

    bool operator==(int v) const {
        return value == v;
    }
};

#include <iostream>

int main() {
    Foo foo{42};

    std::cout << std::boolalpha << (42 != foo);
}
```

All in all, you just have to write the primary operators and all operations with secondary operators will work. Example:

```cpp
#include <compare>

struct Foo {
    const int value;

    bool operator==(int v) const {
        return value == v;
    }

    auto operator<=>(int v) const {
        return value <=> v;
    }
};

#include <iostream>

int main() {
    Foo foo{42};

    std::cout << std::boolalpha << (42 != foo) << '\n';

    std::cout << std::boolalpha << (42 <= foo) << '\n';
    std::cout << std::boolalpha << (42 < foo) << '\n';

    std::cout << std::boolalpha << (42 >= foo) << '\n';
    std::cout << std::boolalpha << (42 > foo) << '\n';
}
```

Output:

```bash
true
false
true
false
true
false
```

## Strong vs Weak vs Partial Ordering

A last point I have to talk about is the return type of the spaceship operator. As stated in the section ["Three-way comparison" of the "Operator Comparison" on cppreference](https://en.cppreference.com/w/cpp/language/operator_comparison):

> The three-way comparison operator expressions have the form
>
> `lhs <=> rhs` (1)
>
> The expression returns an object such that
>
> - `(a <=> b) < 0` if `lhs < rhs`
> - `(a <=> b) > 0` if `lhs > rhs`
> - `(a <=> b)== 0` if `lhs` and `rhs` are equal/equivalent.

Nevertheless, this operator doesn't return an integer and this is why the `<compare>` header must be included to define it:

```cpp
//#include <compare>

struct Foo {
    const int value;

    auto operator<=>(const Foo&) const = default;
};

int main() {
    Foo foo{42};
    Foo bar{66};

    return foo <=> bar;
}
```

```bash
<source>: In member function 'constexpr auto Foo::operator<=>(const Foo&) const':
<source>:6:10: error: 'strong_ordering' is not a member of 'std'
    6 |     auto operator<=>(const Foo&) const = default;
      |          ^~~~~~~~
<source>:1:1: note: 'std::strong_ordering' is defined in header '<compare>'; did you forget to '#include <compare>'?
```

The auto-deduced return type is `std::strong_ordering` here and it is defined in `<compare>`.

If you uncomment the `#include`, you will get another error:

```bash
<source>:13:16: error: cannot convert 'std::strong_ordering' to 'int' in return
   13 |     return foo <=> bar;
      |            ~~~~^~~~~~~
      |                |
      |                std::strong_ordering
```

The type can be compared to but not converted to an integer. You have to change the return statement:

| Statement  |  Value |
|---|---|
| `return (foo <=> bar) == 0;` | 0 |
| `return (foo <=> foo) == 0;` | 1 |
| `return (bar <=> bar) == 0;` | 1 |
| `return (bar <=> foo) == 0;` | 0 |
| `return (bar <=> foo) > 0;` | 1 |
| `return (bar <=> foo) < 0;` | 0 |
| `return (foo <=> bar) > 0;` | 0 |
| `return (foo <=> bar) < 0;` | 1 |

[`std::strong_ordering`](https://en.cppreference.com/w/cpp/utility/compare/strong_ordering) is not always the return type of the spaceship operator. It is also possible that it returns [std::weak_ordering](https://en.cppreference.com/w/cpp/utility/compare/weak_ordering) or [std::partial_ordering](https://en.cppreference.com/w/cpp/utility/compare/partial_ordering).

What are the differences between them?

Here is what Barry Revzin says in its article (you should really read it, it's great 😉):

> `strong_ordering`: a total ordering, where equality implies substitutability (that is `(a <=> b) == strong_ordering::equal` implies that for reasonable functions `f`, `f(a) == f(b)`. “Reasonable” is deliberately underspecified – but shouldn’t include functions that return the address of their arguments or do things like return the `capacity()` of a `vector`, etc. We want to only look at “salient” properties – itself very underspecified, but think of it as referring to the value of a type. The value of a `vector` is the elements it contains, not its address, etc.). The values are `strong_ordering::greater`, `strong_ordering::equal`, and `strong_ordering::less`.
>
> `weak_ordering`: a total ordering, where equality actually only defines an equivalence class. The canonical example here is case-insensitive string comparison – where two objects might be `weak_ordering::equivalent` but not actually equal (hence the naming change to equivalent).
>
> `partial_ordering`: a partial ordering. Here, in addition to the values `greater`, `equivalent`, and `less` (as with `weak_ordering`), we also get a new value: `unordered`. This gives us a way to represent partial orders in the type system: `1.f <=> NaN` is `partial_ordering::unordered`.

## Conclusion

When I started to write, I though it would be an easy-to-write article about the spaceship operator. Damned it wasn't! 😨

C++20 really changes the deal with comparison operators. It is now possible to default operators and heterogeneous operations are handled by the compiler. In my opinion, this is a real improvement in the language that will help to write readable, easy-to-use code and reduce bugs.