25 февраля автор курса Разработчик C++ в Яндекс.Практикуме Георгий Осипов рассказал о новом этапе языка C++ Стандарте C++20. В лекции сделан обзор всех основных нововведений Стандарта, рассказывается, как их применять уже сейчас и чем они могут быть полезны.
При подготовке вебинара стояла цель сделать обзор всех ключевых возможностей C++20. Поэтому вебинар получился насыщенным. Он растянулся почти на 2,5 часа. Для вашего удобства мы разбили текст на шесть частей:
- Модули и краткая история C++.
- Операция космический корабль.
- Концепты.
- Ranges.
- Корутины.
- Другие фичи ядра и стандартной библиотеки. Заключение.
Первую часть на Хабре встретили живо, а в комментариях развязались жаркие дискуссии. Не может не радовать, что так много пользователей следят за развитием языка практически в реальном времени.
Это вторая часть, рассказывающая об операции космический корабль в современном C++.
Операция космический корабль
В C++ теперь свой космос!
Мотивация
В C++ шесть операций сравнения:
- меньше,
- больше,
- меньше или равно,
- больше или равно,
- равно,
- не равно.
Они все выражаются через одно любое неравенство. Но написать эти операции всё равно придётся. И это проблема, которую решает космический корабль.
Предположим, вы определили структуру, содержащую одно число:
struct X { int a;};
Мы хотим сделать так, чтобы значения этой структуры можно было сравнивать друг с другом. Для этого придётся написать шесть операций:
bool operator== (X l, X r) { return l.a == r.a; }bool operator!= (X l, X r) { return l.a != r.a; }bool operator>= (X l, X r) { return l.a >= r.a; }bool operator<= (X l, X r) { return l.a <= r.a; }bool operator< (X l, X r) { return l.a < r.a; }bool operator> (X l, X r) { return l.a > r.a; }
А теперь представьте, что мы хотим сравнивать элементы этой структуры не только между собой, но также с числами
int
. Количество операций возрастает с шести до 18:
bool operator== (X l, int r) { return l.a == r; }bool operator!= (X l, int r) { return l.a != r; }bool operator>= (X l, int r) { return l.a >= r; }bool operator<= (X l, int r) { return l.a <= r; }bool operator< (X l, int r) { return l.a < r; }bool operator> (X l, int r) { return l.a > r; }bool operator== (int l, X r) { return l == r.a; }bool operator!= (int l, X r) { return l != r.a; }bool operator>= (int l, X r) { return l >= r.a; }bool operator<= (int l, X r) { return l <= r.a; }bool operator< (int l, X r) { return l < r.a; }bool operator> (int l, X r) { return l > r.a; }
Что делать? Можно позвать штурмовиков. Их много, и они быстро напишут 18 операций.
Или воспользоваться космическим кораблём. Эту новую операцию в C++ называют космический корабль, потому что она на него похожа: <=>. Более формальное название трёхстороннее сравнение фигурирует в документах Стандарта.
Пример
В структуру
X
я добавил всего одну строчку,
определяющую операцию <=>
. Заметьте, что я даже
не написал, что именно она делает:
#include <iostream>struct X { auto operator<=>(const X&) const = default; // <-- ! int a;};
И C++ всё сделал за меня. Это сработает и в более сложных случаях, например, когда у
X
несколько полей и базовых классов.
При этом всё, что есть в X
, должно поддерживать
сравнение. После того, как я написал эту магическую строчку, я могу
сравнивать объекты X
любым способом:
int main() { X x1{1}, x42{42}; std::cout << (x1 < x42 ? "x1 < x42" : "not x1 < x42") << std::endl; std::cout << (x1 > x42 ? "x1 > x42" : "not x1 > x42") << std::endl; std::cout << (x1 <= x42 ? "x1 <= x42" : "not x1 <= x42") << std::endl; std::cout << (x1 >= x42 ? "x1 >= x42" : "not x1 >= x42") << std::endl; std::cout << (x1 == x42 ? "x1 == x42" : "not x1 == x42") << std::endl; std::cout << (x1 != x42 ? "x1 != x42" : "not x1 != x42") << std::endl;}
Получилась корректная программа. Её можно собрать и запустить. Текстовый вывод выглядит так:
x1 < x42not x1 > x42x1 <= x42not x1 >= x42not x1 == x42x1 != x42
Операция космического корабля сработает и для сравнения элемента структуры
X
с числом. Но придётся написать реализацию.
На этот раз C++ не сможет придумать её за вас. В реализации
воспользуемся встроенной операцией <=>
для
чисел:
#include <iostream>struct X { auto operator<=>(const X&) const = default; auto operator<=>(int r) const { // <-- ! return this->a <=> r; } int a;};
Правда, возникает проблема. C++ создаст не все операции. Если вы определили эту операцию не через
default
, а написали
сами, проверка на равенство и неравенство не будет добавлена. Кто
знает причины пишите в комменты.
int main() { X x1{1}, x42{42}; std::cout << (x1 < 42 ? "x1 < 42" : "not x1 < 42") << std::endl; std::cout << (x1 > 42 ? "x1 > 42" : "not x1 > 42") << std::endl; std::cout << (x1 <= 42 ? "x1 <= 42" : "not x1 <= 42") << std::endl; std::cout << (x1 >= 42 ? "x1 >= 42" : "not x1 >= 42") << std::endl; std::cout << (x1 == 42 ? "x1 == 42" : "not x1 == 42") << std::endl; // <--- ошибка std::cout << (x1 != 42 ? "x1 != 42" : "not x1 != 42") << std::endl; // <--- ошибка}
Впрочем, никто не запрещает определить эту операцию самостоятельно. Ещё одно нововведение C++20: можно добавить проверку только на равенство, а неравенство добавится автоматически:
#include <iostream>struct X { auto operator<=>(const X&) const = default; bool operator==(const X&) const = default; auto operator<=>(int r) const { return this->a <=> r; } bool operator==(int r) const { // <-- ! return operator<=>(r) == 0; } int a;};int main() { X x1{1}, x42{42}; std::cout << (x1 < 42 ? "x1 < 42" : "not x1 < 42") << std::endl; std::cout << (x1 > 42 ? "x1 > 42" : "not x1 > 42") << std::endl; std::cout << (x1 <= 42 ? "x1 <= 42" : "not x1 <= 42") << std::endl; std::cout << (x1 >= 42 ? "x1 >= 42" : "not x1 >= 42") << std::endl; std::cout << (x1 == 42 ? "x1 == 42" : "not x1 == 42") << std::endl; std::cout << (x1 != 42 ? "x1 != 42" : "not x1 != 42") << std::endl;}
Хоть 2 операции и пришлось определить, но это гораздо лучше, чем 18.
Мы добавили код для тех ситуаций, когда левый операнд это
X
, а правый int
. Оказывается, сравнение в
другую сторону писать не нужно, оно добавится автоматически:
#include <iostream>struct X { auto operator<=>(const X&) const = default; bool operator==(const X&) const = default; auto operator<=>(int r) const { return this->a <=> r; } bool operator==(int r) const { // <-- ! return operator<=>(r) == 0; } int a;};int main() { X x1{1}, x42{42}; std::cout << (1 < x42 ? "1 < x42" : "not 1 < x42") << std::endl; std::cout << (1 > x42 ? "1 > x42" : "not 1 > x42") << std::endl; std::cout << (1 <= x42 ? "1 <= x42" : "not 1 <= x42") << std::endl; std::cout << (1 >= x42 ? "1 >= x42" : "not 1 >= x42") << std::endl; std::cout << (1 == x42 ? "1 == x42" : "not 1 == x42") << std::endl; std::cout << (1 != x42 ? "1 != x42" : "not 1 != x42") << std::endl;}
Теория
На этом рассказ про космический корабль можно было бы завершить, но, оказывается, не всё так просто. Есть ещё несколько нюансов.
Первый: всё, что я сказал это неправда. Никаких операций сравнения на самом деле не добавилось. Если вы попробуете явно вызвать операцию меньше, компилятор скажет: Ошибка. Такой операции нет. Несмотря на то, что сравнение работает, получить адрес операции меньше не получится:
#include <iostream>struct X { auto operator<=>(const X&) const = default; int a;};int main() { X x1{1}, x42{42}; std::cout << (x1.operator<(x42) ? "<" : "!<") // <--- ошибка << std::endl; }
Удивительно, как же компилятор выполняет операцию, которой нет. Всё благодаря тому, что поменялись правила поведения компилятора при вычислении операций сравнения. Когда вы пишете
x1 <
x2
, компилятор, как и раньше, проверяет наличие операции
<
. Но теперь, если он её не нашёл, то обязательно
посмотрит операцию космического корабля. В примере она находится,
поэтому он её использует. При этом, если типы операндов разные,
компилятор посмотрит сравнение в обе стороны: сначала в одну, потом
в другую. Поэтому нет необходимости определять третий космический
корабль для сравнения int
и типа X
достаточно определить только вариант, где X
слева.Если вам по какой-то причине вместо
x < y
нравится
писать x.operator<(y)
, то определите операцию
<
явно. У меня для вас хорошие новости: реализацию
можно не писать. default
будет работать для обычных
операций сравнения так же, как и для <=>
.
Напишите его, и C++ определит его за вас. Вообще, C++20 многое
делает за вас.
#include <iostream>struct X { auto operator<=>(const X&) const = default; bool operator<(const X&) const = default; // <-- ! int a;};int main() { X x1{1}, x42{42}; std::cout << (x1.operator<(x42) ? "<" : "!<") << std::endl;}
Заметьте, что у
operator<
потребовалось указать
явный тип возврата bool
. А в <=>
эту работу предоставляли компилятору, указывая auto
.
Оно означает, что тип я писать не хочу: компилятор умный, он поймёт
сам, что нужно поставить вместо auto
. Но какой-то тип
там есть функция же должна что-то возвращать.Оказывается, тут не всё так просто. Это не
bool
, как
для простых операций сравнения. Здесь сразу три варианта. Эти
варианты разные виды упорядочивания:std::strong_ordering
. Линейный порядок, равные элементы которого неразличимы. Примеры:int
,char
,string
.std::weak_ordering
. Линейный порядок, равные могут быть различимы. Примеры:string
, сравниваемый без учёта регистра; порядок на точках плоскости, определяемый удалённостью от центра.std::partial_ordering
. Частичный порядок. Примеры:float
,double
, порядок по включению на объектахset
.
Математики хорошо знакомы с этими понятиями, тут даже нет никакого программирования. Для остальных расскажем. Линейный порядок такой, при котором любые два элемента можно сравнить между собой. Пример линейного порядка целые числа: какие бы два числа мы ни взяли, их можно сравнить между собой.
При частичном порядке элементы могут быть несравнимы. Числа с плавающей запятой
float
и double
подпадают под понятие частичного порядка, потому что у них есть
специальное значение NaN, не сравнимое ни с каким другим
числом.Дальнейшие рассуждения об упорядочивании выходят за рамки вебинара. Я лишь хочу сказать, что не всё так тривиально, как кажется. Рекомендую поэкспериментировать с частичным упорядочиванием в разных алгоритмах и контейнерах типа
set
.Статус
Космический корабль уже есть везде, и им можно пользоваться:
- GCC. Хорошо поддерживается с версии 10, хотя и не до конца. Полную поддержку обещают только в GCC 11.
- Clang. Полная поддержка в версии 10.
- Visual Studio. Полная поддержка в VS 2019.
Заключение
Во время трансляции мы опросили аудиторию, нравится ли ей эта функция. Результаты опроса:
- Суперфича 47 (87.04%)
- Так себе фича 2 (3.70%)
- Пока неясно 5 (9.26%)
Мы довольно подробно разобрали операцию космический корабль, однако всё равно часть её функций осталась непокрытой. Например, интересный вопрос: как компилятор обработает ситуацию, в которой определено несколько операций сравнения с разными типами.
Читателям Хабра, как и слушателям вебинара, дадим возможность оценить нововведения.