The C++20 spaceship operator and its implementation in the standard library

The spaceship operator has finally been incorporated into the standard library. Very soon you will be able to use this new operator for classes like vector, string, array and others. This has not gone without discussion, problems and last minute changes to the standard. One of the problems of the operator could only be solved by splitting up this operator and having two different comparison operator for each of the STL classes: operator== and operator<=>.

Since we have two comparison operators now, using an std::vector in your own class could look like this:

class MyVector
{
	public:
	friend auto operator<=>(const MyVector& v1, const MyVector& v2)
	{
		return v1.vector <=> v2.vector;
	}
	
	friend auto operator==(const MyVector& v1, const MyVector& v2)
	{
		return v1.vector == v2.vector;
	}
	
	std::vector<int> vector;
};

Keep in mind that we can also use default to have the compiler generate these functions for us:

class MyVector
{
	public:
	auto operator<=>(const MyVector&) const = default;
	auto operator==(const MyVector&) const = default;
	
	std::vector<int> vector;
};

In this article, I will further discuss the background of the earlier problems with the spaceship operator and I will show why using two operators instead of just one is needed to solve these problems

The problem with having only the three-way comparison operator: inefficient coding pitfalls

One of the main disadvantages of implementing all comparisons using the three-way comparison (spaceship) operator is the inability to include more efficient equality (and inequality) functions when declaring an operator<=> for certain classes
[P1185R0][D0790R1].

An example of this inability to implement the most efficient equality functions is given now. Consider again our storage type MyVector, which stores a vector of numbers and defines a three-way comparison operator like this:

#include <compare>
#include <vector>

template <typename T>
std::strong_ordering operator<=>(const std::vector<T>& v1, const std::vector<T>& v2);

class MyVector
{
    public:
    friend auto operator<=>(const MyVector& s1, const MyVector& s2)
    {
	return s1.numbers <=> s2.numbers;
    }
    
    private:
    std::vector<int> numbers;
};

// An example of what operator<=> would look like on a vector.
template <typename T>
std::strong_ordering operator<=>(const std::vector<T>& v1, const std::vector<T>& v2)
{
    size_t smallest_size = std::min(v1.size(), v2.size());
    for(size_t i = 0; i < smallest_size; ++i)
    {
        const std::strong_ordering comparison = v1[i] <=> v2[i];
        if(0 != comparison) return comparison;
    }
    return v1.size() <=> v2.size();
}
Note: Clang already supports (parts of) the spaceship operator. I recommend using wandbox.org to try out the above code.

In this code, the sizes of both vectors are only compared at the end, as these are only important for the < and > operator after all elements have been checked.

This highlights the problem of our spaceship operator: When an equality (==) check is done, the sizes should be compared before iterating through the vector values, not after checking all elements first.

When the spaceship operator is used for all comparisons, it means that we have no way of adding optimizations for certain comparison (e.g. equality checking). To make it possibly to optimize equality and inequality comparisons, the spaceship operator has to be changed.

The solution: breaking up the spaceship

Breaking up the spaceship operator has been mentioned in [P1185R0] and [D0790R1]. These papers proposed the following changes to solve this problem of missed optimizations:

  • Separate operator== from operator<=>. When declaring a default operator<=>, the equality (operator==) and inequality (operator!=) operators should no longer be automatically generated as well. When you want to generate the equality and inequality operator, you will have to declare a default operator== as well. This change allows you to write your own optimized version of these operators.
  • Change the lookup behaviour for == and != operators. The proposal specifies that <=> should not be called at all when an equality or inequality comparison is done. This means no more accidental calls to inefficient comparison code.

Current status

As of March, the solutions proposed in [PP1185] have been acknowledged and are imported into the new standard. In [P1614R0], a description of changes to the standard library is given. As we can see in the paper, most (if not all) classes in the standard library have the new spaceship operator defined: this would mean that, once implemented, you could use the spaceship operator on strings, vectors, arrays and other classes.Take for example vector, which now has the following two comparison operators: operator== and operator<=>.

So, if you want all comparison operators to be defined, make sure you define both:

auto operator<=>(const MyVector&) const = default;
auto operator==(const MyVector&) const = default;

Conclusion

We have seen a few shortcomings in the original proposal for the three-way comparison operator. These shortcomings have now been addressed and the standard library has received code changes for this new operator. If all goes well, we should soon be able to experiment with these new changes in the standard library.