C++ и космические технологии

0981e04ed00e78a57052df6da409a5d4

В сегодняшней публикации мы поговорим о новом новшестве в мире C++ — операторе «спейсшип» (spaceship aka three-way comparison, он же тройное сравнение.
Устраивайтесь поудобнее, взлетаем.

Итак, оператор '<=>' появился в C++20.
Что же он делает?
Обычный оператор сравнения вроде '<' берет на вход два значения, тестирует на них корректность заданного бинарного отношения и возвращает булево значение, обозначающее результат проверки.
Например, для выражения '5<2' очевидно, что отношение '<' здесь не осуществляется, поэтому результат будет false.
Просто и понятно.

выражение '5<=>2' действует по другому. Оно вычисляет само отношение (в данном случае '<') и возвращает его.
Прямо скажем, функция не самая важная и в других языках давно реализованная.

Давайте теперь взглянем на нюансы использования этой операции.

Нюанс #1

Возьмем простенький код

int main(){
    auto n=5;
    auto m=2;
    bool a = n

Он скомпилируется?
Конечно.

А такой код, тоже простенький?

int main(){
    auto n=5;
    auto m=2;
    auto a = n<=>m;
    return 0;
}

Такой код уже не скомпилируется.
Почему?
Потому что надо добавить хедер.

#include 
int main(){
    auto n=5;
    auto m=2;
    auto a = n<=>m;
    return 0;
}

Обратите внимание: если вы хотите использовать спейсшип и не использовать стандартную библиотеку вообще — придется выбрать что-то одно.
Совместить не получится.

Почему же так?
О. Тут хитро. '<' и '<=>' оба встроенные операторы языка. Но одному не требуется никакой внешний хедер, а другому таки да. Дело в том, что '<=>' возвращает значения, определенные в стандартной библиотеке. И вместо того, чтобы вынести сам оператор в стандартную библиотеку, разработчики стандарта затолкали его в язык, а необходимые ему для работы значения вынесли наружу. Почему они так сделали, я лично не знаю, но предчувствую плохое.

Нюанс #2

Опять простой код

#include 
int main(){
    auto n=5;
    auto m=2;
    auto a = n<=>m;

    auto c=2.0;
    auto d=1.0;
    auto e=c<=>d;

    bool eq=(e==a);  // returns true  

    return 0;
}

Что мы здесь имеем?
Мы производим 2 сравнения с разными типами. И получаем 2 разных класса ответов -std: strong_ordering и std: partial_ordering.
На самом деле есть даже три разных класса ответов — добавьте std: weak_ordering.
Причем, что небезынтересно, в 2 классах есть по 4 стандартных значения, а в одном 3.
Третий класс мы тут трогать не будем, потрогаем только первые два.
Так вот, оказывается, что в одном (std: strong_ordering) 4 значения, из которых 2 на самом деле эквивалентны (по-моему, даже побитово).
Но в целом значения одного класса не совсем эквивалентны другому — 4 значения одного класса отображаются в 3 значения другого!

Здорово, правда?
Но это еще не все.

Нюанс #3

Простой код:

auto c=2.0;
auto d=1.0;
auto e=c<=>d;

if(e==std::partial_ordering::equivalent){
        std::cout<<"e==eqvuivalent "<

Скомпилируется? Ну конечно!

А так?

auto c=2.0;
auto d=1.0;
auto e=c<=>d;

if(e==0){
        std::cout<<"e==equivalent "<

Да, да, да!

А так?

auto c=2.0;
auto d=1.0;
auto e=c<=>d;

if(e==0){
        std::cout<<"e==equivalent "<

А?

А так уже нет.
Оказывается, с нулем сравнивать можно, а с остальными числами низя.
Почему? Потому.

Зато с нулем — сравнивай не хочу:

auto c=2.0;
auto d=1.0;
auto e=c<=>d;
   
if(e==0){
        std::cout<<"e==eqvuivalent "<0){
        std::cout<<"e==great "<

Нюанс #4

auto dnan = std::nan("1");
auto dnancomp = d<=>dnan;
bool t = dnan==0.1;

Какое значение будет иметь t? A dnancomp?

t у нас будет false, а dnancomp будет иметь значение 'unordered'

Что означает, цитирую: a valid value of the type std::partial_ordering indicating relationship with an incomparable value

Это немножко вызывает недоумение.
Как же так, я ж в другой строке выяснил, что nan не равен числу, а тут вдруг оказывается, что значения несравнимые?
Как говорила Ф.Раневская, мы значения сравниваем, а они, оказывается, несравнимые!
Почему нельзя было добавить в std: partial_ordering еще и значение «non_equivalent»? Гулять так гулять, где 4 значения, там и пятое можно всунуть.

Нюанс #5

Как нетрудно сообразить, эту космическую приблуду нетрудно сэмулировать.
Например, такой код:

auto n=5;
auto m=2;
auto a = n<=>m;

Легко можно заменить таким:

auto n=5;
auto m=2;
if(n>m){
  //greater
}
else if(n

Возможны вариации.
Но с космическим оператором жизнь становится краше: теперь надо проверить к какому классу относится результат (просто, чтобы знать, какими значениями можно пользоваться), а потом проверить какое значение выпало.
Ну как надо — необязательно, но может возникнуть такая ситуация.
В чем выигрыш по сравнению с тремя стариковскими строчками (см. выше) — даже не знаю.

Нюанс #6

Нам столько говорили про то, как благотворно влияют новые стандарты на перформанс, что захотелось проверить.
Никаких бенчмарков проводить не буду, ограничусь толко сравнением ассемблеров.

Вот традиционный, он же стариковско-пещерный код:

int main(){
    int x=0;
    auto n=2;
    auto m=1;
  
    if(n>m){
        x=1;
    }
    else if(m>n){
        x=2;
    }
    else{
        x=3;
    }
  return 0;
  }

Вот его ассемблер:

main:
        push    rbp
        mov     rbp, rsp
        mov     DWORD PTR [rbp-4], 0
        mov     DWORD PTR [rbp-8], 2
        mov     DWORD PTR [rbp-12], 1
        mov     eax, DWORD PTR [rbp-8]
        cmp     eax, DWORD PTR [rbp-12]
        jle     .L2
        mov     DWORD PTR [rbp-4], 1
        jmp     .L3
.L2:
        mov     eax, DWORD PTR [rbp-12]
        cmp     eax, DWORD PTR [rbp-8]
        jle     .L4
        mov     DWORD PTR [rbp-4], 2
        jmp     .L3
.L4:
        mov     DWORD PTR [rbp-4], 3
.L3:
        mov     eax, 0
        pop     rbp
        ret

А это молодежно-космический код:

#include 
int main(){
    int x=0;
    auto n=2;
    auto m=1;
    auto a = n<=>m;
    if(a==std::strong_ordering::greater){
        x=1;
    }
    else if(a==std::strong_ordering::less){
        x=2;
    }
    else{
        x=3;
    }
  return 0;
  }

Здесь я обработал только три значения — если бы речь шла о классе std: partial, пришлось бы обработать 4.

И его ассемблер:

std::strong_ordering::less:
        .byte   -1
std::strong_ordering::greater:
        .byte   1
main:
        push    rbp
        mov     rbp, rsp
        sub     rsp, 16
        mov     DWORD PTR [rbp-4], 0
        mov     DWORD PTR [rbp-8], 2
        mov     DWORD PTR [rbp-12], 1
        mov     edx, DWORD PTR [rbp-8]
        mov     eax, DWORD PTR [rbp-12]
        cmp     edx, eax
        je      .L5
        cmp     edx, eax
        jge     .L6
        mov     BYTE PTR [rbp-13], -1
        jmp     .L7
.L6:
        mov     BYTE PTR [rbp-13], 1
        jmp     .L7
.L5:
        mov     BYTE PTR [rbp-13], 0
.L7:
        mov     edx, 1
        movzx   eax, BYTE PTR [rbp-13]
        mov     esi, edx
        mov     edi, eax
        call    std::operator==(std::strong_ordering, std::strong_ordering)
        test    al, al
        je      .L8
        mov     DWORD PTR [rbp-4], 1
        jmp     .L9
.L8:
        mov     edx, -1
        movzx   eax, BYTE PTR [rbp-13]
        mov     esi, edx
        mov     edi, eax
        call    std::operator==(std::strong_ordering, std::strong_ordering)
        test    al, al
        je      .L10
        mov     DWORD PTR [rbp-4], 2
        jmp     .L9
.L10:
        mov     DWORD PTR [rbp-4], 3
.L9:
        mov     eax, 0
        leave
        ret

(Микрофон падает)

Finale

Если очень легкомысленно подытожить мои личные впечатления от космической технологии, то будет примерно так: работа проделана огромная, результаты … не очень.
Мне кажется, что фича относительно не очень важная, и реализована она как-то размашисто с одной стороны, и корявенько с другой.

Разве что использовать ее в компайл-тайме, может там у нее есть какие-то важные плюсы… Не знаю, не копал.

А вы как думаете?

© Habrahabr.ru