The Spaceship Operator and Expression Rewriting

A guide to simplifying our comparison operators using automatic expression rewriting, and the new spaceship operator added in C++20
This lesson is part of the course:

Professional C++

Comprehensive course covering advanced concepts, and how to use them on large-scale projects.

ds7.jpg
Ryan McCombe
Ryan McCombe
Posted

Let's imagine we have the following custom type, which simply stores a value, and implements an == operator:

class Number {
public:
  bool operator==(const Number& Other) const {
    std::cout << "Hello from the == operator\n";
    return Value == Other.Value;
  }

  int Value;
};

We create two objects of this type, and compare them using the != operator:

int main(){
  Number A{1};
  Number B{2};

  if (A != B) { std::cout << "Not equal!"; }
}

Our type doesn’t have the != operator, so we’d expect this to fail. In C++17 and earlier, that’s exactly what happens:

error: binary '!=':
'Number' does not define this operator

But, from C++20 onwards, this program will compile, and run as we expect:

Hello from the == operator
Not equal!

This works because, behind the scenes, the compiler has rewritten our expression.

Expression Rewriting

As the previous output would indicate, the expression using the != operator is actually calling the == operator of our class.

This is an example of expression rewriting, which was added to comparison operators in C++20

Specifically, if our type doesn't have the != operator, the compiler can rewrite any expression using it in terms of the == iterator. So, A != B will be rewritten to !(A == B)

Specifically, any expression using a secondary comparison operator can be rewritten in terms of the corresponding primary comparison operator.

  • The secondary comparison operator != corresponds to the primary comparison operator ==
  • The secondary comparison operators !=, <, <=, > and >= correspond to the primary comparison operator <=>, which we’ll cover next

Three-Way Comparison using the <=> operator

The three-way comparison operator was added in C++20, and uses the syntax <=>.

A <=> B

Because of its visual appearance, it is often called the Spaceship Operator

The <=> operator accepts two operands, and returns one of three possible values. The return value depends on whether the left operand is less than, equal to, or greater than the right operand.

These three possibilities are contained within the std::strong_ordering struct:

  • If A < B it returns std::strong_ordering::less
  • If A == B it returns std::strong_ordering::equal
  • If A > B it returns std::strong_ordering::greater

Equivalence vs Equality

There is a fourth possible result of comparison - std::strong_ordering::equivalent. In the vast majority of cases, there is no difference between equality and equivalence. For the purpose of boolean operations, they are the same thing.

There may just be some niche cases where we want to differentiate two different “levels” of equality. For example:

  • if we’re creating a string-like type, we may want to consider two objects equal if they’re an exact match, but still equivalent if they match on a case-insensitive basis
  • we may want to define two operands of our custom type to be equal if they are the exact same object (i.e., same memory address), but still equivalent if they’re different objects with the same value

In code, we could use the <=> operator and std::strong_ordering type like this:

if (A <=> B == std::strong_ordering::less) {
  std::cout << "A is less than B";
}

if (A <=> B == std::strong_ordering::equal) {
  std::cout << "A is equal to B";
}

if (A <=> B == std::strong_ordering::greater) {
  std::cout << "A is greater than B";
}

The previous example shows the mechanics of the <=> operator and the std::strong_ordering type, but it’s quite unusual that we’d use them like this. In fact, the <=> syntax, and std::strong_ordering reference will typically only be used in one place - in our class code, to define the three-way comparison operation.

Any other code seeking to compare our objects will still be using the regular comparison operators, like > and >= to return booleans. However, as of C++20, these operators no longer need to be defined. The compiler can rewrite any expression that attempts to use them to secretly call the <=> operator instead:

#include <iostream>

class Number {
public:
  // Our class defines the <=> operator
  // which returns a strong_ordering...
  std::strong_ordering operator<=>(
    const Number& Other) const{
    std::cout << "Hello from <=>\n";
    return Value <=> Other.Value;
  }

  int Value;
};

int main(){
  Number A{1};
  Number B{2};

  // ...but our consumers use the normal
  // comparison operators to return booleans
  if (A < B) { std::cout << "A < B\n"; }
  if (A <= B) { std::cout << "A <= B\n"; }
}
Hello from <=>
A < B
Hello from <=>
A <= B

In summary, as of C++20, we now only need to define the two primary comparison operators for our types: == and <=>. All of the secondary comparison operators can be automatically generated from these.

Why can’t == and != be rewritten in terms of <=>?

It seems that any expression using the == and != operators could also be rewritten in terms of the spaceship operator <=>.

This is true, but it’s not done automatically. The primary reason for this is that, for most types, == can be implemented in a more efficient way than <=>. This is because determining whether two objects are equal is usually easier, and therefore more performant, than determining their relative ordering.

If we attempt to use a == operator that doesn’t exist, C++ won’t rewrite our expression in a way that’s probably slower than it should be. Especially as we might not notice that’s what is happening - we might just assume the type has a == operator defined.

So instead, the compiler tells us the operator doesn’t exist. We can then define it, and we can even define it in terms of the <=> operator if that’s what we really want:

bool operator==(const T& Left, const T& Right){
  return Left <=> Right ==
    std::strong_ordering::equal;
}

Was this lesson useful?

Edit History

  • — First Published

Ryan McCombe
Ryan McCombe
Posted
This lesson is part of the course:

Professional C++

Comprehensive course covering advanced concepts, and how to use them on large-scale projects.

7a.jpg
This lesson is part of the course:

Professional C++

Comprehensive course covering advanced concepts, and how to use them on large-scale projects.

Free, unlimited access!

This course includes:

  • 106 Lessons
  • 550+ Code Samples
  • 96% Positive Reviews
  • Regularly Updated
  • Help and FAQ
Next Lesson

C++20 Modules

A detailed overview of C++20 modules - the modern alternative to #include directives. We cover import and export statements, partitions, submodules, how to combine modules with legacy code, and more.
812.jpg
Contact|Privacy Policy|Terms of Use
Copyright © 2023 - All Rights Reserved