How do I use the spaceship operator in a switch statement

458 Views Asked by At

The new <=> operator makes writing code more convenient and it can save some performance if the comparison algorithm is non-trivial, because it doesn't need to be repeated two times to get the full ordering.

Or at least I though so when I learned about it. However, when I try to use it in practice, in a switch statement, it doesn't work.

This code doesn't compile:

#include <iostream>

void compare_values(int x, int y)
{
    switch (x <=> y)
    {
    case std::strong_ordering::less:
        std::cout << "is less\n";
        break;
    case std::strong_ordering::greater:
        std::cout << "is greater\n";
        break;
    case std::strong_ordering::equal:
        std::cout << "is equal\n";
        break;
    }
}

The compiler shows an error suggesting that the value returned by <=> cannot be used in a switch:

<source>: In function 'void compare_values(int, int)':
<source>:5:15: error: switch quantity not an integer
    5 |     switch (x <=> y)
      |             ~~^~~~~
Compiler returned: 1

(live example)

I would guess that using the spaceship operator in switch is a pretty basic, obvious and common use case, so there is probably some trick to making it work. I cannot figure it out, though.

How can I fix this code?

2

There are 2 best solutions below

21
wohlstad On BEST ANSWER

The problem is that the spaceship operator (formally known as three-way comparison) does not return an ingeral type, and therefore cannot be used in a switch-case statement.

In this case of comparing types like ints, the spaceship operator returns a std::strong_ordering.

(A side note: as you can see in the documentation there are cases where the spaceship operator returns a std::partial_ordering but this isn't an integral type either).

You can use it in an if-else statement instead.

If you prefer to use a switch-case, you can use a trivial utility that converts the std::strong_ordering to an integral type with some predefined values.
Returning -1/0/1 in this case will be quite natural:

#include <iostream>

constexpr int strong_ordering_to_int(std::strong_ordering o)
{
    if (o == std::strong_ordering::less)    return -1;
    if (o == std::strong_ordering::greater) return 1;
    return 0;
}

void compare_values(int x, int y)
{
    switch (strong_ordering_to_int(x <=> y))
    {
    case -1:
        std::cout << "is less\n";
        break;
    case 1:
        std::cout << "is greater\n";
        break;
    case 0:
        std::cout << "is equal\n";
        break;
    }
}

int main()
{
    compare_values(2, 3);
    compare_values(3, 2);
    compare_values(3, 3);
}

Output:

is less
is greater
is equal

Live demo

7
Beached On

Compilers do a bad job of optimizing using if to extract the value of the ordering types(gcc/clang/MSVC) see https://gcc.godbolt.org/z/P7scbPqGT.

to_integral(std::__1::strong_ordering):
        xor     ecx, ecx
        test    dil, dil
        setne   cl
        cmp     dil, -1
        mov     eax, 255
        cmovne  eax, ecx
        ret

The best way to get good codegen(see https://gcc.godbolt.org/z/b8Pf1GErP)

to_integral(std::__1::strong_ordering):
        mov     eax, edi
        ret

is to play on the big 3 implementations using an underlying char value member. One can easily add static_asserts with the strong_ordering/weak_ordering values to ensure this. The following will optimize to at worst a copy. https://gcc.godbolt.org/z/4En7sbsoE

#include <compare>
#include <bit>

enum class cmp_result: signed char { less = -1, equivalent = 0, equal = 0, greater = 1 };

constexpr cmp_result to_integral( std::strong_ordering x ) {
    return std::bit_cast<cmp_result>( x );
}
constexpr cmp_result to_integral( std::weak_ordering x ) {
    return std::bit_cast<cmp_result>( x );
}

static_assert( to_integral( std::strong_ordering::less ) == cmp_result::less );
static_assert( to_integral( std::strong_ordering::equal ) == cmp_result::equal );
static_assert( to_integral( std::strong_ordering::greater ) == cmp_result::greater );

static_assert( to_integral( std::weak_ordering::less ) == cmp_result::less );
static_assert( to_integral( std::weak_ordering::equivalent ) == cmp_result::equivalent );
static_assert( to_integral( std::weak_ordering::greater ) == cmp_result::greater );

With this we can now efficiently switch over the spaceship operator.

void compare_values(int x, int y)
{
    switch (to_integral(x <=> y))
    {
    case cmp_result::less:
        std::cout << "is less\n";
        break;
    case cmp_result::greater:
        std::cout << "is greater\n";
        break;
    case cmp_result::equal:
        std::cout << "is equal\n";
        break;
    }
}