Featured image of post C++20 Three-Way Comparison Operator(Spaceship Operator) Usage Guide

C++20 Three-Way Comparison Operator(Spaceship Operator) Usage Guide

Analysis of C++20's three-way comparison operator (spaceship operator), its principles, usage, and best practices. Compare implementations before and after C++20 with practical examples to master efficient usage of modern C++ comparison operators.

C++20 introduces the three-way comparison operator (also known as the spaceship operator), written as <=>. It simplifies and unifies comparison operations.

Basic Concepts

The <=> operator performs a comparison between two values in one operation and returns one of three possible results:

  • less - (a <=> b) < 0 if a < b
  • equal/equivalent - (a <=> b) == 0 if a and b are equal/equivalent
  • greater - (a <=> b) > 0 if a > b

Return Types

It returns a value representing the comparison result, which can be directly used for sorting and equality checking.

<=> returns one of the following three types:

  1. std::strong_ordering - Strong ordering

    • Values: less, equal, greater
    • Applicable to: integers, pointers, etc.
  2. std::weak_ordering - Weak ordering

    • Values: less, equivalent, greater
    • Applicable to: case-insensitive string comparisons
  3. std::partial_ordering - Partial ordering

    • Values: less, equivalent, greater, unordered
    • Applicable to: floating-point numbers (due to NaN)

Motivation

When first encountering the <=> operator, you might be confused about its purpose, use cases, and other questions.

Before C++20, to compare custom types, we typically had to do this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
struct Point {
    int x, y;
  
    bool operator==(const Point& rhs) const {
        return x == rhs.x && y == rhs.y;
    }

    bool operator!=(const Point& rhs) const {
        return !(*this == rhs);
    }
  
    bool operator<(const Point& rhs) const {
        if (x != rhs.x)
            return x < rhs.x;
        return y < rhs.y;
    }
  
    bool operator<=(const Point& rhs) const {
        return !(rhs < *this);
    }
  
    bool operator>(const Point& rhs) const {
        return rhs < *this;
    }
  
    bool operator>=(const Point& rhs) const {
        return !(*this < rhs);
    }
};

int main() {
    Point p1{1, 2}, p2{1, 3};

    std::cout << std::boolalpha << (p1 < p2) << '\n'; // true
    std::cout << std::boolalpha << (p1 == p2) << "\n"; // false
}

That is, we needed to implement 6 comparison operators to enable comparison of custom types. After defining a <=> operator, the compiler automatically generates all six comparison operators (<, <=, >, >=, ==, !=).

Basic Usage

Comparing primitive types:

1
2
3
4
5
6
7
8
int main() {
    int a = 5, b = 10;
    std::strong_ordering result = a <=> b;
  
    if (result < 0) {
        std::cout << "a < b\n";
    }
}

Comparing custom types:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
#include <compare>
#include <iostream>

struct Point {
    int x, y;
  
    // Only need to define the <=> operator
    auto operator<=>(const Point& other) const = default;
};

int main() {
    Point p1{1, 2}, p2{1, 3};
  
    if (p1 < p2) {
        std::cout << "p1 < p2\n";
    }
  
    return 0;
}

Custom comparison logic:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
struct Person
{
    std::string name;
    int age;

    // Compare by age first, then by name if equal
    auto operator<=>(const Person& rhs) const
    {
        if (auto cmp = age <=> rhs.age; cmp != 0)
            return cmp;
        return name <=> rhs.name;
    }

    // Still need to explicitly define ==
    bool operator==(const Person& rhs) const = default;
};

Person p1{"Alice", 30}, p2{"Bob", 25};
std::cout << std::boolalpha << (p1 >= p2) << '\n'; // true (30 > 25)
std::cout << std::boolalpha << (p1 == p2) << '\n'; // false (30 != 25)

[!note] When you define a custom operator<=>, you need to explicitly define the operator== operator, because operator<=> does not automatically generate operator==. This is primarily for performance considerations: <=> may perform unnecessary ordering comparisons, while == only needs to check for equality.

Official explanation:

Once a type has <=>, == is generated from <=>. For strings, == is typically optimized by first comparing sizes: if the number of characters differs, the strings are not equal. An == generated from <=> would have to read enough of the strings to determine their lexicographical order, which would be much more expensive. After extensive discussion, we decided not to generate == from <=>. - From “Thriving in a Crowded and Changing World”

Summary

  1. Code Simplification: After defining a <=> operator, the compiler automatically generates all six comparison operators (<, <=, >, >=, ==, !=)

  2. Performance Optimization: A single comparison yields the complete ordering relationship, avoiding multiple comparisons

  3. Default Implementation: Using = default allows the compiler to automatically generate member-wise lexicographical comparison

https://www.stroustrup.com/hopl20main-p5-p-bfc9cd4--final.pdf

https://github.com/Cpp-Club/Cxx_HOPL4_zh

Licensed under CC BY-NC-SA 4.0