Хабрахабр

[Перевод] Новый оператор spaceship (космический корабль) в C++20

C++20 добавляет новый оператор, названный «космическим кораблем»: <=>. Не так давно Simon Brand опубликовал пост, в котором содержалась подробная концептуальная информация о том, чем является этот оператор и для каких целей используется. Главной задачей этого поста является изучение конкретных применений «странного» нового оператора и его аналога operator==, а также формирование некоторых рекомендаций по его использованию в повседневном кодинге.

Сравнение

Нет ничего необычного в том, чтобы увидеть код, подобный следующему:

struct IntWrapper { } bool operator==(const IntWrapper& rhs) const { return value == rhs.value; } bool operator!=(const IntWrapper& rhs) const { return !(*this == rhs); } bool operator<(const IntWrapper& rhs) const { return value < rhs.value; } bool operator<=(const IntWrapper& rhs) const { return !(rhs < *this); } bool operator>(const IntWrapper& rhs) const { return rhs < *this; } bool operator>=(const IntWrapper& rhs) const { return !(*this < rhs); }
};

Примечание: внимательные читатели заметят, что это на самом деле даже менее многословно, чем должно быть в коде до версии C++20. Подробнее об этом позже.

Хорошо, мы разберемся с этим за какое-то время. Нужно написать много стандартного кода, чтобы убедиться, что наш тип сопоставим с чем-то такого же типа. Затем приходит кто-то, кто пишет так:

constexpr bool is_lt(const IntWrapper& a, const IntWrapper& b) { return a < b;
}
int main() { static_assert(is_lt(0, 1));
}

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

error C3615: constexpr function 'is_lt' cannot result in a constant expression

Затем некоторые добавят constexpr во все операторы сравнения. Проблема в том, что был забыт constexpr в функции сравнения. Несколько дней спустя кто-то добавит помощник is_gt, но заметит, что все операторы сравнения не имеют спецификации исключений, и придется проходить один и тот же утомительный процесс добавления noexcept к каждой из 5 перегрузок.

Давайте посмотрим, как можно написать исходный IntWrapper в мире C++20: Именно здесь в помощь нам приходит новый оператор C++20 spaceship.

#include <compare>
struct IntWrapper { int value; constexpr IntWrapper(int value): value{value} { } auto operator<=>(const IntWrapper&) const = default;
};

Первое отличие, которое вы можете заметить — это новое включение <compare>. Заголовок <compare> отвечает за заполнение компилятора всеми типами категорий сравнения, необходимыми для оператора spaceship, чтобы он возвращал тип, подходящий для нашей дефолтной функции. В приведенном выше фрагменте тип возвращаемого значения auto будет std::strong_ordering.

is_lt остается неизменным и просто работает, оставаясь при этом constexpr, хотя мы не указали это явно в нашем дефолтном operator<=>. Мы не только удалили 5 лишних строк, но нам даже не нужно ничего определять, компилятор сделает это за нас. Давайте найдем ответ на этот вопрос. Это хорошо, но некоторые люди могут ломать голову над тем, почему is_lt разрешено компилировать, даже если он вообще не использует оператор spaceship.

Переписывание выражений

В C++20 компилятор вводится в новую концепцию, имеющую отношение к «переписанным» выражениям. Оператор spaceship, наряду с operator==, является одним из первых двух кандидатов, которые могут быть переписаны. Для более конкретного примера переписывания выражений давайте разберем пример, приведенный в is_lt.

Процесс отбора кандидатов изменяется очень незначительно для случая операций сравнения и операций эквивалентности, когда компилятор также должен собирать специальных переписанных и синтезированных кандидатов ([over.match.oper]/3. Во время разрешения перегрузки компилятор будет выбирать из набора наиболее подходящих кандидатов, каждый из которых соответствует оператору, который нам нужен. 4).

Так делает компилятор и обнаруживает, что на самом деле тип a содержит IntWrapper::operator<=>. Для нашего выражения a < b стандарт утверждает, что мы можем искать тип a для operator<=> или функции operator<=>, которые принимают этот тип. Это переписанное выражение затем используется в качестве кандидата для нормального разрешения перегрузки. Затем компилятору разрешается использовать этот оператор и переписать выражение a < b как (a <=> b) < 0.

Правильность выражения фактически вытекает из семантики, которую обеспечивает оператор spaceship. Вы можете спросить, почему это переписанное выражение является корректным. Если у вас есть порядок, вы можете выразить этот порядок в терминах любых операций сравнения. <=> — это трехстороннее сравнение, которое подразумевает, что вы получаете не просто бинарный результат, но и порядок (в большинстве случаев). Результат std::strong_ordering::less подразумевает, что 4 не только отличается от 5 но и строго меньше этого значения, что делает применение операции (4 <=> 5) < 0 правильным и точным для описания нашего результата. Быстрый пример, выражение 4 <=> 5 в C++20 вернет вам результат std::strong_ordering::less.

<, >, и т.д.) и переписать его в терминах оператора spaceship. Используя приведенную выше информацию, компилятор может взять любой обобщенный оператор сравнения (т.е. В стандарте переписанное выражение часто упоминается как (a <=> b) @ 0 где @ представляет любую операцию сравнения.

Синтезирующие выражения

Читатели, возможно, заметили тонкое упоминание «синтезированных» выражений выше, и они также играют роль в этом процессе переписывания операторов. Рассмотрим следующую функцию:

constexpr bool is_gt_42(const IntWrapper& a) { return 42 < a;
}

Если мы используем наше первоначальное определение для IntWrapper, этот код не будет компилироваться.

error C2677: binary '<': no global operator found which takes type 'const IntWrapper' (or there is no acceptable conversion)

Если вы попробуете построить этот пример с помощью компилятора и определения IntWrapper C++20, вы можете заметить, что он, опять же, просто работает. Это имеет смысл до версии C++20, и способ решения этой проблемы заключается в добавлении некоторых дополнительных функций friend в IntWrapper , которые занимают левую сторону от int. Давайте рассмотрим, почему приведенный выше код все еще компилируется в C++20.

В приведенном выше примере компилятор попытается использовать переписанное выражение (42 <=> a) < 0, но обнаружит, что нет преобразования из IntWrapper в int, чтобы удовлетворить левую часть, так что переписанное выражение отбрасывается. Во время разрешения перегрузки компилятор также будет собирать то, что стандарт называет «синтезированными» кандидатами, или переписанным выражением с обратным порядком параметров. Компилятор также вызывает «синтезированное» выражение 0 < (a <=> 42) и обнаруживает, что происходит преобразование из int в IntWrapper через его конструктор преобразования, поэтому этот кандидат используется.

Синтезированные выражения обобщаются до 0 @ (b <=> a). Цель синтезированных выражений состоит в том, чтобы избежать путаницы в необходимости написания шаблонов функций friend , чтобы заполнить пробелы, в которых ваш объект может быть преобразован из других типов.

Более сложные типы

Сгенерированный компилятором оператор spaceship не останавливается на отдельных членах классов, он генерирует правильный набор сравнений для всех подобъектов в ваших типах:

struct Basics { int i; char c; float f; double d; auto operator<=>(const Basics&) const = default;
}; struct Arrays { int ai[1]; char ac[2]; float af[3]; double ad[2][2]; auto operator<=>(const Arrays&) const = default;
}; struct Bases : Basics, Arrays { auto operator<=>(const Bases&) const = default;
}; int main() { constexpr Bases a = { { 0, 'c', 1.f, 1. }, { { 1 }, { 'a', 'b' }, { 1.f, 2.f, 3.f }, { { 1., 2. }, { 3., 4. } } } }; constexpr Bases b = { { 0, 'c', 1.f, 1. }, { { 1 }, { 'a', 'b' }, { 1.f, 2.f, 3.f }, { { 1., 2. }, { 3., 4. } } } }; static_assert(a == b); static_assert(!(a != b)); static_assert(!(a < b)); static_assert(a <= b); static_assert(!(a > b)); static_assert(a >= b);
}

Компилятор знает, как развернуть члены классов, которые являются массивами, в их списки подобъектов и сравнить их рекурсивно. Конечно, если вы хотите написать тела этих функций самостоятельно, вы все равно получите пользу от переписывания выражений компилятором.

Выглядит как утка, плавает как утка, и крякает как operator==

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

Если у вас есть строка "foobar" и вы сравниваете ее со строкой "foo", используя ==, можно ожидать, что эта операция будет почти постоянной. Канонический пример со сравнением двух строк. Эффективный алгоритм сравнения строк следующий:

  • Сначала сравните размер двух строк. Если размеры отличаются, то верните false
  • В противном случае пошагово просматривайте каждый элемент двух строк и сравнивайте их до тех пор, пока не найдется отличие или не закончатся все элементы. Верните результат.

В соответствии с правилами оператора spaceship мы должны начать с сравнения каждого элемента, пока не найдем тот, который отличается. В нашем примере "foobar" и "foo" только при сравнении 'b' и '\0' вы наконец возвращаете false.

Наш IntWrapper может быть написан следующим образом: Для борьбы с этим была статья P1185R2, в которой подробно описывается, как компилятор переписывает и генерирует operator== независимо от оператора spaceship.

#include <compare>
struct IntWrapper { int value; constexpr IntWrapper(int value): value{value} { } auto operator<=>(const IntWrapper&) const = default; bool operator==(const IntWrapper&) const = default;
};

Еще один шаг… однако, есть хорошие новости; вам на самом деле не нужно писать код выше, потому что простого написания auto operator<=>(const IntWrapper&) const = default достаточно, чтобы компилятор неявно сгенерировал отдельный и более эффективный operator== для вас!

Это означает, что != также выигрывает от оптимизации. Компилятор применяет слегка измененное правило «перезаписи», специфичное для == и !=, где в этих операторах они переписываются в терминах operator==, а не operator<=>.

Старый код не сломается

В этот момент вы можете подумать: хорошо, если компилятору разрешено выполнять эту операцию переписывания операторов, что произойдет, если я попытаюсь перехитрить компилятор:

struct IntWrapper { int value; constexpr IntWrapper(int value): value{value} { } auto operator<=>(const IntWrapper&) const = default; bool operator<(const IntWrapper& rhs) const { return value < rhs.value; }
};
constexpr bool is_lt(const IntWrapper& a, const IntWrapper& b) { return a < b;
}

Ответ — вы не сможете. Модель разрешения перегрузки в C++ имеет арену, в которой сражаются все кандидаты. В этом конкретном сражении у нас есть 3 кандидата:

  • IntWrapper::operator<(const IntWrapper& a, const IntWrapper& b)
  • IntWrapper::operator<=>(const IntWrapper& a, const IntWrapper& b)

(переписанный)

  • IntWrapper::operator<=>(const IntWrapper& b, const IntWrapper& a)

(синтезированный)

Если бы мы приняли правила разрешения перегрузки в C++17, результат этого вызова был бы неоднозначным, но правила разрешения перегрузки C++20 были изменены, чтобы компилятор мог разрешить эту ситуацию до наиболее логичной перегрузки.

В C ++20 появился новый механизм тай-брейкинга, в рамках которого предпочтение отдается перегрузкам, которые не переписываются и не синтезируются, что делает нашу перегрузку IntWrapper::operator<лучшим кандидатом и разрешает неоднозначность. Существует фаза разрешения перегрузки, когда компилятор должен выполнить серию тай-брейков. Этот же механизм предотвращает полное замещение регулярных переписанных выражений синтезированными кандидатами.

Заключительные мысли

Оператор spaceship является желанным дополнением к C++, поскольку сможет помочь упростить ваш код и писать его меньше, а иногда меньше — лучше. Так что пристегивайтесь и управляйте космическим кораблем C++20!

Как примечание, изменения, внесенные в P1185R2, будут доступны в Visual Studio 2019 версии 16. Мы призываем вас выйти и опробовать оператор spaceship, он доступен прямо сейчас в Visual Studio 2019 под /std:c++latest! Пожалуйста, имейте в виду, что оператор spaceship является частью C++20 и подвержен некоторым изменениям вплоть до того момента, когда C++20 будет финализирован. 2.

Не стесняйтесь присылать любые комментарии по электронной почте по адресу visualcpp@microsoft.com, через Twitter @visualc, или Facebook Microsoft Visual Cpp. Как всегда, мы ждем ваших отзывов.

Для предложений или сообщией об ошибках, пишите нам через DevComm. Если вы столкнулись с другими проблемами с MSVC в VS 2019, сообщите нам об этом через опцию «Сообщить о проблеме», либо из установщика, либо из самой Visual Studio IDE.

Показать больше

Похожие публикации

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»