C++20: More Details to the Spaceship Operator

Contents[Show]

The compiler performs quite a clever job when it generates all six comparison operators. In the end, you get the intuitive and efficient comparison operators for free. Let me dive with this post into the details of the spaceship operator.

 TimelineCpp20

First, I want to add something which I should have written about in my first post to the three-way comparison operator: "C++20: The Three-Way Comparisio Operator".

Direct Usage of the Three-Way Comparison Operator

You can directly use the spaceship operator:

 

// spaceship.cpp

#include <compare>
#include <iostream>
#include <string>
#include <vector>

int main() {
    
    std::cout << std::endl;
    
    int a(2011);
    int b(2014);
    auto res = a <=> b;                 // (1)
    if (res < 0) std::cout << "a < b" << std::endl;
    else if (res == 0) std::cout << "a == b" << std::endl;
    else if (res > 0) std::cout << "a > b" << std::endl;

    std::string str1("2014");
    std::string str2("2011");
    auto res2 = str1 <=> str2;          // (2)
    if (res2 < 0) std::cout << "str1 < str2" << std::endl;
    else if (res2 == 0) std::cout << "str1 == str2" << std::endl;
    else if (res2 > 0) std::cout << "str1 > str2" << std::endl;
    
    std::vector<int> vec1{1, 2, 3};
    std::vector<int> vec2{1, 2, 3};
    auto res3 = vec1 <=> vec2;          // (3)
    if (res3 < 0) std::cout << "vec1 < vec2" << std::endl;
    else if (res3 == 0) std::cout << "vec1 == vec2" << std::endl;
    else if (res3 > 0) std::cout << "vec1 > vec2" << std::endl;
    
    std::cout << std::endl;
    
}

 

You can directly use the spaceship operator for int's (1), for string's (2), and for vector's (3). Thanks to the wandbox online-compiler and the newest GCC, here is the output of the program.

spaceship

Now, it's time for something new in C++. C++20 introduces the concept of "rewritten" expressions.

Rewriting Expressions

When the compiler sees something such as a < b, it rewrites it to (a <=> b) < 0 using the spaceship operator.

Of course, the rule applies to all six comparison operators:

a OP b becomes (a <=> b) OP 0. It's even better. If there is no conversion of the type(a) to the type(b), the compiler generates the new expression 0 OP (b <=> a).

For example, this means for the less-than operator, if (a <=> b) < 0 does not work, the compiler generates 0 < (b <=> a). In essence, the compiler takes automatically care of the symmetry of the comparison operators.

Here are a few examples of the rewriting expressions:

 

// rewrittenExpressions.cpp

#include <compare>
#include <iostream>

class MyInt {
 public:
    constexpr MyInt(int val): value{val} { }
    auto operator<=>(const MyInt& rhs) const = default;  
 private:
    int value;
};

int main() {
    
    std::cout << std::endl;
    
    constexpr MyInt myInt2011(2011);
    constexpr MyInt myInt2014(2014);
   
    constexpr int int2011(2011);
    constexpr int int2014(2014);
    
    if (myInt2011 < myInt2014) std::cout << "myInt2011 < myInt2014" << std::endl;          // (1)
    if ((myInt2011 <=> myInt2014) < 0) std::cout << "myInt2011 < myInt2014" << std::endl; 
    
    std::cout << std::endl;
    
    if (myInt2011 < int2014) std:: cout << "myInt2011 < int2014" << std::endl;             // (2)
    if ((myInt2011 <=> int2014) < 0) std:: cout << "myInt2011 < int2014" << std::endl;
    
    std::cout << std::endl;
    
    if (int2011 < myInt2014) std::cout << "int2011 < myInt2014" << std::endl;              // (3)
    if (0 < (myInt2014 <=> int2011)) std:: cout << "int2011 < myInt2014" << std::endl;     // (4)
    
    std::cout << std::endl;
    
}
   

 

I used in (1), (2), and (3) the less-than operator and the corresponding spaceship expression. (4) is the most interesting example. It exemplifies, how the comparison (int2011 < myInt2014) triggers the generation of the spaceship expression (0 < (myInt2014 <=> int2011).

To be honest, MyInt has an issue.  Constructor taking one argument should be explicit.

Explicit Constructor

Constructors taking one argument such as MyInt(int val) are conversion constructors. This means in the concrete case that an instance from MyInt can be generated from any integral or floating-point value because each integral or floating-point value can implicitly be converted to int. I assume that you don't want implicit conversion from an integral or a floating-point value when an instance of MyInt is required.

First Try

To disable this implicit conversion, I make the constructor explicit following the Python meta-rule: explicit is better than implicit. The following program shows the explicit constructor:

 

// threeWayComparisonWithInt1.cpp

#include <compare>
#include <iostream>

class MyInt {
 public:
    constexpr explicit MyInt(int val): value{val} { }
    auto operator<=>(const MyInt& rhs) const = default;  
 private:
    int value;
};

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

int main() {
    
    std::cout << std::boolalpha << std::endl;
    
    constexpr MyInt myInt2011(2011);
    constexpr MyInt myInt2014(2014);
    
    constexpr int int2011(2011);
    constexpr int int2014(2014);
    
    std::cout << "isLessThan(myInt2011, myInt2014): "
              << isLessThan(myInt2011, myInt2014) << std::endl;
              
    std::cout << "isLessThan(int2011, myInt2014): "
              << isLessThan(int2011, myInt2014) << std::endl;  // (3)
            
    std::cout << "isLessThan(myInt2011, int2014): "
              << isLessThan(myInt2011, int2014) << std::endl;  // (2)
              
    constexpr auto res = isLessThan(myInt2011, int2014);
              
    std::cout << std::endl;
              
}

 

This was easy. Thanks to the explicit constructor, the implicit conversion from int to MyInt in (1) is not valid anymore. The compiler speaks now an unambiguous message.

threeWayComparisonWithInt1

When you read carefully the error message, you notice that there is no operator < for a right-hand operand int available and no conversion from int to MyInt possible. Interestingly, the compiler complains about (2), but not about (3). Both functions calls cause a compiler error.

Second Try

To support the comparison from MyInt's and int's, MyInt needs an additional three-way comparison operator.

 

#include <compare>
#include <iostream>

class MyInt {
 public:
    constexpr explicit MyInt(int val): value{val} { }
    auto operator<=>(const MyInt& rhs) const = default; // (4) 
    constexpr auto operator<=>(const int& rhs) const {  // (1)
        return value <=> rhs;
    }
 private: 
    int value;
};

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

int main() {
    
    std::cout << std::boolalpha << std::endl;
    
    constexpr MyInt myInt2011(2011);
    constexpr MyInt myInt2014(2014);
    
    constexpr int int2011(2011);
    constexpr int int2014(2014);
    
    std::cout << "isLessThan(myInt2011, myInt2014): "
              << isLessThan(myInt2011, myInt2014) << std::endl; // (3) 
              
    std::cout << "isLessThan(int2011, myInt2014): "
              << isLessThan(int2011, myInt2014) << std::endl;   // (3)
            
    std::cout << "isLessThan(myInt2011, int2014): "
              << isLessThan(myInt2011, int2014) << std::endl;   // (3)
              
    constexpr auto res = isLessThan(myInt2011, int2014);        // (2)
              
    std::cout << std::endl;
              
}

 

I defined in (1) the three-way comparison operator and declared it constexpr. The user-defined three-way comparison operator is in contrast to the compiler-generated three-way comparison operator not constexpr. Consequently, I can perform the isLessThan (4) call at compile-time. The comparison of MyInt's and int's is possible in each combination (3).

threeWayComparisonWithInt2

 

To be honest, I find the implementation of the various three-way comparison operators very elegant. The compiler auto-generates the comparison of MyInt's, and the user defines the comparison with int's explicitly.  Additionally, you have to define only 2 operators to get 18 = 3 * 6 combinations of comparison operators. 3 stands for the combination of int's and MyInt's and 6 for the six comparison operators. I discussed in my last post "C++20: The Three-Way Comparisio Operator"  the 18 operators you had to overload before C++20.

I want to make one point clear: You can even compare MyInt which each type that is convertible to int.

Stop! You may ask yourself: What is the current implementation using an explicit constructor

class MyInt {
 public:
    constexpr explicit MyInt(int val): value{val} { }
    auto operator<=>(const MyInt& rhs) const = default; 
    constexpr auto operator<=>(const int& rhs) const {  
        return value <=> rhs;
    }
 private: 
    int value;
};

 

better than the previous implementation using a constructor capable of implicit conversions? Both classes allow comparisons with integrals and floating-point values.

class MyInt {
 public:
    constexpr MyInt(int val): value{val} { }
    auto operator<=>(const MyInt& rhs) const = default;  
 private:
    int value;
};W

 

What's next?

There is a subtle difference between an explicit and a non-explicit constructor for MyInt that you can easily see when I make MyInt more int-like in my next post. Additionally, the compiler-generated the == and != operators are special for performance reasons and the interplay of classical comparison operators and the three-way comparison operator are worth an extra post.

 

 

Thanks a lot to my Patreon Supporters: Matt Braun, Roman Postanciuc, Tobias Zindl, Marko, G Prvulovic, Reinhold Dröge, Abernitzke, Frank Grimm, Sakib, Broeserl, António Pina, Sergey Agafyin, Андрей Бурмистров, Jake, GS, Lawton Shoemake, Animus24, Jozo Leko, John Breland, espkk, Louis St-Amour, Venkat Nandam, Jose Francisco, Douglas Tinkham, Kuchlong Kuchlong, Robert Blanch, Truels Wissneth, Kris Kafka, Mario Luoni, Neil Wang, Friedrich Huber, lennonli, Pramod Tikare Muralidhara, Peter Ware, Tobi Heideman, Daniel Hufschläger, Red Trip, Alexander Schwarz, Tornike Porchxidze, Alessandro Pezzato, Evangelos Denaxas, Bob Perry, Satish Vangipuram, Andi Ireland, Richard Ohnemus, Michael Dunsky, Dimitrov Tsvetomir, Leo Goodstadt, Eduardo Velasquez, John Wiederhirn, Yacob Cohen-Arazi, Florian Tischler, Robin Furness, and Michael Young.

 

Thanks in particular to Jon Hess, Lakshman, Christian Wittenhorst, Sherhy Pyton, Dendi Suhubdy, Sudhakar Belagurusamy, Richard Sargeant, and Rusty Fleming.

 

 

My special thanks to Embarcadero CBUIDER STUDIO FINAL ICONS 1024 Small

 

Seminars

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

Bookable (Online)

German

Standard Seminars (English/German)

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

New

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

Interactive Course: The All-in-One Guide to C++20

Subscribe to the newsletter (+ pdf bundle)

Blog archive

Source Code

Visitors

Today 5391

Yesterday 8162

Week 22261

Month 140463

All 7408303

Currently are 156 guests and no members online

Kubik-Rubik Joomla! Extensions

Latest comments