C++20: The Three-Way Comparison Operator

Contents[Show]

The three-way comparison operator <=> is often just called spaceship operator. The spaceship operator determines for two values A  and B whether A < B, A = B, or A > B. You can define the spaceship operator or the compiler can auto-generate it for you.

 TimelineCpp20

To appreciate the advantages of the three-way comparison operator, let me start classical.

Ordering before C++20

I implemented a simple int wrapper MyInt. Of course, I want to compare MyInt. Here is my solution using the isLessThan function template.

 

// comparisonOperator.cpp

#include <iostream>

struct MyInt {
    int value;
    explicit MyInt(int val): value{val} { }
    bool operator < (const MyInt& rhs) const {                  
        return value < rhs.value;
    }
};

template <typename T>
constexpr bool isLessThan(const T& lhs, const T& rhs) {
    return lhs < rhs;
}

int main() {

    std::cout << std::boolalpha << std::endl;

    MyInt myInt2011(2011);
    MyInt myInt2014(2014);

    std::cout << "isLessThan(myInt2011, myInt2014): "
              << isLessThan(myInt2011, myInt2014) << std::endl;

   std::cout << std::endl;

}

 

The program works as expected:

comparisonOperator

Honestly, MyInt is an unintuitive type. When you define one of the six ordering relations, you should define all of them. Intuitive types should be at least semi-regular: "C++20: Define the Concept Regular and SemiRegular."

Now, I have to write a lot of boilerplate code. Here are the missing five operators:

 

bool operator==(const MyInt& rhs) const { 
    return value == rhs.value; 
}
bool operator!=(const MyInt& rhs) const { 
    return !(*this == rhs);    
}
bool operator<=(const MyInt& rhs) const { 
    return !(rhs < *this);     
}
bool operator>(const MyInt& rhs)  const { 
    return rhs < *this;        
}
bool operator>=(const MyInt& rhs) const {   
    return !(*this < rhs);     
}

 

Done? No! I assume you want to compare MyInt with int's. To support the comparison of an int and a MyInt, and a MyInt and an int, you have to overload each operator three times because the constructor is declared as explicit. Thanks to explicit, no implicit conversion from int to MyInt kicks in. For convenience, you make the operators to a friend of the class. If you need more background information for my design decisions, read my previous post: "C++ Core Guidelines: Rules for Overloading and Overload Operators"

These are the three overloads for smaller-than.

 

friend bool operator < (const MyInts& lhs, const MyInt& rhs) {                  
    return lhs.value < rhs.value;
}

friend bool operator < (int lhs, const MyInt& rhs) {                  
    return lhs < rhs.value;
}

friend bool operator < (const MyInts& lhs, int rhs) {                  
    return lhs.value < rhs;
}

 

This means in total that you have to implement 18 comparison operators. Is this the end of the story? Maybe not, because you decided that the MyInt and all operators should become constexpr. You should also consider making the operators noexcept.

I assume this is enough motivation for the three-way comparison operators.

Ordering with C++20

You can define the three-way comparison operator or request it from the compiler with =default. In both cases you get all six comparison operators: ==, !=, <, <=, >, and >=.

 

// threeWayComparison.cpp

#include <compare>
#include <iostream>

struct MyInt {
    int value;
    explicit MyInt(int val): value{val} { }
    auto operator<=>(const MyInt& rhs) const {           // (1)      
        return value <=> rhs.value;
    }
};

struct MyDouble {
    double value;
    explicit constexpr MyDouble(double val): value{val} { }
    auto operator<=>(const MyDouble&) const = default;   // (2)
};

template <typename T>
constexpr bool isLessThan(const T& lhs, const T& rhs) {
    return lhs < rhs;
}

int main() {
    
    std::cout << std::boolalpha << std::endl;
    
    MyInt myInt1(2011);
    MyInt myInt2(2014);
    
    std::cout << "isLessThan(myInt1, myInt2): "
              << isLessThan(myInt1, myInt2) << std::endl;
              
    MyDouble myDouble1(2011);
    MyDouble myDouble2(2014);
    
    std::cout << "isLessThan(myDouble1, myDouble2): "
              << isLessThan(myDouble1, myDouble2) << std::endl;          
              
    std::cout << std::endl;
              
}

 

The user-defined (1) and the compiler-generated (2) three-way comparison operator work as expected.

threeWayComparison

But there are a few subtle differences in this case. The by the compiler deduced return type for MyInt (1) supports strong ordering, and the by the compiler deduced return type of MyDouble supports partial ordering. Floating pointer numbers only support partial ordering because floating-point values such as NaN (Not a Number) can not be ordered. For example NaN == NaN  is false.

Now, I want to focus on this post about the compiler-generated spaceship operator.

The Compiler-Generated Spaceship Operator

The compiler-generated three-way comparison operator needs the header <compare>, is implicit constexpr and noexcept. Additionally, it performs a lexicographical comparison. What? Let me start with constexpr.

Comparison at Compile-Time

The three-way comparison operator is implicit constexpr. Consequently, I simplify the previous program threeWayComparison.cpp and compare MyDouble in the following program at compile-time.

 

// threeWayComparisonAtCompileTime.cpp

#include <compare>
#include <iostream>

struct MyDouble {
    double value;
    explicit constexpr MyDouble(double val): value{val} { }
    auto operator<=>(const MyDouble&) const = default;    
};

template <typename T>
constexpr bool isLessThan(const T& lhs, const T& rhs) {
    return lhs < rhs;
}

int main() {
    
    std::cout << std::boolalpha << std::endl;

              
    constexpr MyDouble myDouble1(2011);
    constexpr MyDouble myDouble2(2014);
    
    constexpr bool res = isLessThan(myDouble1, myDouble2); // (1)
    
    std::cout << "isLessThan(myDouble1, myDouble2): "
              << res << std::endl;          
              
    std::cout << std::endl;
              
}

 

I ask for the result of the comparison at compile-time (1), and I get it.

threeWayComparisonAtComileTime

 

The compiler-generated three-way comparison operator performs a lexicographical comparison. 

Lexicographical Comparison

Lexicographical comparison means in this case that all base classes are compared left to right and all non-static members of the class in their declaration order. I have to qualify: for performance reasons, the compiler-generated == and != operator behave differently in C++20. I will write about this exception to the rule in my next post.

The post "Simplify Your Code With Rocket Science: C++20’s Spaceship Operator" Microsoft C++ Team Blog provides an impressive example to the lexicographical comparison.

 

struct Basics {
  int i;
  char c;
  float f;
  double d;
  auto operator<=>(const Basics&) const = default;
};

struct Arrays {
  int ai[1];
  char ac[2];
  float af[3];
  double ad[2][2];
  auto operator<=>(const Arrays&) const = default;
};

struct Bases : Basics, Arrays {
  auto operator<=>(const Bases&) const = default;
};

int main() {
  constexpr Bases a = { { 0, 'c', 1.f, 1. },    // (1)
                        { { 1 }, { 'a', 'b' }, { 1.f, 2.f, 3.f }, { { 1., 2. }, { 3., 4. } } } };
  constexpr Bases b = { { 0, 'c', 1.f, 1. },   // (1)
                        { { 1 }, { 'a', 'b' }, { 1.f, 2.f, 3.f }, { { 1., 2. }, { 3., 4. } } } };
  static_assert(a == b);
  static_assert(!(a != b));
  static_assert(!(a < b));
  static_assert(a <= b);
  static_assert(!(a > b));
  static_assert(a >= b);
}

I assume, the most complex aspect of the program is not the spaceship operator, but the initialization of Base via aggregate initialization (1). Aggregate initialization enables it to directly initialize the members of a class type (class, struct, union) when the members are all public.  In this case, you can use brace initialization. If you want to know more about aggregate initialization, cppreference.com provides more information. I will write more about aggregate initialization in a future post when I will have a closer look at designated initialization in C++20.

What's next?

The compiler performs quite a clever job when it generates all operators. On the end, you get the intuitive and efficient comparison operators for free. My next post dives deeper into the magic under the hood.

 

 

Thanks a lot to my Patreon Supporters: Meeting C++, Matt Braun, Roman Postanciuc, Venkata Ramesh Gudpati, Tobias Zindl, Marko, G Prvulovic, Reinhold Dröge, Abernitzke, Frank Grimm, Sakib, Broeserl, António Pina, Darshan Mody, Sergey Agafyin, Андрей Бурмистров, Jake, GS, Lawton Shoemake, Animus24, Jozo Leko, John Breland, espkk, Wolfgang Gärtner, Jon Hess, Christian Wittenhorst, Louis St-Amour, Stephan Roslen, Venkat Nandam, Jose Francisco, Douglas Tinkham, Lakshman, Kuchlong Kuchlong, and Avi Kohn.

 

Thanks in particular to: Bitwyre Technologies

 

Thanks in particular to:   crp4

 

Seminars

I'm happy to give online-seminars or face-to-face seminars world-wide. Please call me if you have any questions.

Bookable Seminars (Online)

Standard Seminars 

Here is a compilation of my standard seminars. These seminars are only meant to give you a first orientation.

Contact Me

Modernes C++,

RainerGrimmSmall

My Newest E-Books

Course: Modern C++ Concurrency in Practice

Course: C++ Standard Library including C++14 & C++17

Course: Embedded Programming with Modern C++

Course: Generic Programming (Templates)

Course: C++ Fundamentals for Professionals

Subscribe to the newsletter (+ pdf bundle)

Blog archive

Source Code

Visitors

Today 907

All 4358859

Currently are 126 guests and no members online

Kubik-Rubik Joomla! Extensions

Latest comments