Как стать автором
Обновить

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

Уровень сложностиПростой
Время на прочтение5 мин
Количество просмотров9.2K

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

Устраивайтесь поудобнее, взлетаем.

Итак, оператор <=> появился в C++20.

Что же он делает?

Обычный оператор сравнения вроде < берет на вход два значения, тестирует на них корректность заданного бинарного отношения и возвращает булево значение, обозначающее результат проверки.

Например, для выражения 5<2 очевидно, что отношение < здесь не осуществляется, поэтому результат будет false.

Просто и понятно.

Выражение 5<=>2 действует по другому. Оно вычисляет само отношение (в данном случае >) и возвращает его. Т.е, 5<=>2 выдает результат 'greater', потому что 5>2.

Прямо скажем, функция не самая важная и в других языках давно реализованная.

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

Нюанс #1

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

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

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

Конечно.

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

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

Такой код уже не скомпилируется.

Почему?

Потому что надо добавить хедер.

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

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

Совместить не получится.

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

Нюанс #2

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

#include <compare>
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 значения одного класса (std::strong_ordering) отображаются в 3 значения другого (std::partial_ordering)!

Здорово, правда?

Но это еще не все.

Нюанс #3

Простой код:

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

if(e==std::partial_ordering::equivalent){
        std::cout<<"e==eqvuivalent "<<std::endl;
    }
    else if (e==std::partial_ordering::less){
        std::cout<<"e==less "<<std::endl;
    }
    else if (e==std::partial_ordering::greater){
        std::cout<<"e==great "<<std::endl;
    }
    else if (e==std::partial_ordering::unordered){
        std::cout<<"e==unordered "<<std::endl;
    }

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

А так?

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

if(e==0){
        std::cout<<"e==equivalent "<<std::endl;
    }
    else if (e==std::partial_ordering::less){
        std::cout<<"e==less "<<std::endl;
    }
    else if (e==std::partial_ordering::greater){
        std::cout<<"e==great "<<std::endl;
    }
    else if (e==std::partial_ordering::unordered){
        std::cout<<"e==unordered "<<std::endl;
    }

Да, да, да!

А так?

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

if(e==0){
        std::cout<<"e==equivalent "<<std::endl;
    }
    else if (e==1){
        std::cout<<"e==less "<<std::endl;
    }
    else if (e==std::partial_ordering::greater){
        std::cout<<"e==great "<<std::endl;
    }
    else if (e==std::partial_ordering::unordered){
        std::cout<<"e==unordered "<<std::endl;
    }
А?

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

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

auto c=2.0;
auto d=1.0;
auto e=c<=>d;
   
if(e==0){
        std::cout<<"e==eqvuivalent "<<std::endl;
    }
    else if (e<0){
        std::cout<<"e==less "<<std::endl;
    }
    else if (e>0){
        std::cout<<"e==great "<<std::endl;
    }
    else if (e==std::partial_ordering::unordered){
        std::cout<<"e==unordered "<<std::endl;
    }

Нюанс #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<m){
  //less
}
else{
  //equal
}

Возможны вариации.

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

Ну как надо - необязательно, но может возникнуть такая ситуация.

В чем выигрыш по сравнению с тремя стариковскими строчками (см. выше) - даже не знаю.

Нюанс #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 <compare>
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_ordering, пришлось бы обработать 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

Компилятор один и тот же - gcc 13.2 x86-64
Ключи оптимизации - дефолтные в обоих случаях.

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

Finale

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

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

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

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

Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.
Пользовались ли вы уже оператором spaceship?
8.87% Да, много раз.11
6.45% Да, разок или два.8
84.68% Вообще ни разу.105
Проголосовали 124 пользователя. Воздержались 13 пользователей.
Теги:
Хабы:
Всего голосов 10: ↑7 и ↓3+7
Комментарии30

Публикации

Истории

Работа

Программист C++
150 вакансий
QT разработчик
12 вакансий

Ближайшие события

27 августа – 7 октября
Премия digital-кейсов «Проксима»
МоскваОнлайн
28 сентября – 5 октября
О! Хакатон
Онлайн
3 – 18 октября
Kokoc Hackathon 2024
Онлайн
10 – 11 октября
HR IT & Team Lead конференция «Битва за IT-таланты»
МоскваОнлайн
25 октября
Конференция по росту продуктов EGC’24
МоскваОнлайн
7 – 8 ноября
Конференция byteoilgas_conf 2024
МоскваОнлайн