C++20 добавляет новый оператор, названный «космическим кораблем»:
Нет ничего необычного в том, чтобы увидеть код, подобный следующему:
Примечание: внимательные читатели заметят, что это на самом деле даже менее многословно, чем должно быть в коде до версии C++20. Подробнее об этом позже.
Нужно написать много стандартного кода, чтобы убедиться, что наш тип сопоставим с чем-то такого же типа. Хорошо, мы разберемся с этим за какое-то время. Затем приходит кто-то, кто пишет так:
Первое, что вы заметите, это то, что программа не будет компилироваться.
Проблема в том, что был забыт
Именно здесь в помощь нам приходит новый оператор C++20 spaceship. Давайте посмотрим, как можно написать исходный
Первое отличие, которое вы можете заметить — это новое включение
Мы не только удалили 5 лишних строк, но нам даже не нужно ничего определять, компилятор сделает это за нас.
В C++20 компилятор вводится в новую концепцию, имеющую отношение к «переписанным» выражениям. Оператор spaceship, наряду с
Во время разрешения перегрузки компилятор будет выбирать из набора наиболее подходящих кандидатов, каждый из которых соответствует оператору, который нам нужен. Процесс отбора кандидатов изменяется очень незначительно для случая операций сравнения и операций эквивалентности, когда компилятор также должен собирать специальных переписанных и синтезированных кандидатов ([over.match.oper]/3.4).
Для нашего выражения
Вы можете спросить, почему это переписанное выражение является корректным. Правильность выражения фактически вытекает из семантики, которую обеспечивает оператор spaceship.
Используя приведенную выше информацию, компилятор может взять любой обобщенный оператор сравнения (т.е.
Читатели, возможно, заметили тонкое упоминание «синтезированных» выражений выше, и они также играют роль в этом процессе переписывания операторов. Рассмотрим следующую функцию:
Если мы используем наше первоначальное определение для
Это имеет смысл до версии C++20, и способ решения этой проблемы заключается в добавлении некоторых дополнительных функций
Во время разрешения перегрузки компилятор также будет собирать то, что стандарт называет «синтезированными» кандидатами, или переписанным выражением с обратным порядком параметров. В приведенном выше примере компилятор попытается использовать переписанное выражение
Цель синтезированных выражений состоит в том, чтобы избежать путаницы в необходимости написания шаблонов функций
Сгенерированный компилятором оператор spaceship не останавливается на отдельных членах классов, он генерирует правильный набор сравнений для всех подобъектов в ваших типах:
Компилятор знает, как развернуть члены классов, которые являются массивами, в их списки подобъектов и сравнить их рекурсивно. Конечно, если вы хотите написать тела этих функций самостоятельно, вы все равно получите пользу от переписывания выражений компилятором.
Выглядит как утка, плавает как утка, и крякает как
Некоторые очень умные люди в комитете по стандартизации заметили, что оператор spaceship всегда будет выполнять лексикографическое сравнение элементов, несмотря ни на что. Безусловное выполнение лексикографических сравнений может привести к неэффективному коду, в частности, с оператором равенства.
Канонический пример со сравнением двух строк. Если у вас есть строка
В соответствии с правилами оператора spaceship мы должны начать с сравнения каждого элемента, пока не найдем тот, который отличается. В нашем примере
Для борьбы с этим была статья P1185R2, в которой подробно описывается, как компилятор переписывает и генерирует
Еще один шаг… однако, есть хорошие новости; вам на самом деле не нужно писать код выше, потому что простого написания
Компилятор применяет слегка измененное правило «перезаписи», специфичное для
В этот момент вы можете подумать: хорошо, если компилятору разрешено выполнять эту операцию переписывания операторов, что произойдет, если я попытаюсь перехитрить компилятор:
Ответ — вы не сможете. Модель разрешения перегрузки в C++ имеет арену, в которой сражаются все кандидаты. В этом конкретном сражении у нас есть 3 кандидата:
(переписанный)
(синтезированный)
Если бы мы приняли правила разрешения перегрузки в C++17, результат этого вызова был бы неоднозначным, но правила разрешения перегрузки C++20 были изменены, чтобы компилятор мог разрешить эту ситуацию до наиболее логичной перегрузки.
Существует фаза разрешения перегрузки, когда компилятор должен выполнить серию тай-брейков. В C ++20 появился новый механизм тай-брейкинга, в рамках которого предпочтение отдается перегрузкам, которые не переписываются и не синтезируются, что делает нашу перегрузку
Оператор spaceship является желанным дополнением к C++, поскольку сможет помочь упростить ваш код и писать его меньше, а иногда меньше — лучше. Так что пристегивайтесь и управляйте космическим кораблем C++20!
Мы призываем вас выйти и опробовать оператор spaceship, он доступен прямо сейчас в Visual Studio 2019 под
Как всегда, мы ждем ваших отзывов. Не стесняйтесь присылать любые комментарии по электронной почте по адресу visualcpp@microsoft.com, через Twitter @visualc, или Facebook Microsoft Visual Cpp.
Если вы столкнулись с другими проблемами с MSVC в VS 2019, сообщите нам об этом через опцию «Сообщить о проблеме», либо из установщика, либо из самой Visual Studio IDE. Для предложений или сообщией об ошибках, пишите нам через DevComm.
<=>
. Не так давно Simon Brand опубликовал пост, в котором содержалась подробная концептуальная информация о том, чем является этот оператор и для каких целей используется. Главной задачей этого поста является изучение конкретных применений «странного» нового оператора и его аналога operator==
, а также формирование некоторых рекомендаций по его использованию в повседневном кодинге.Сравнение
Нет ничего необычного в том, чтобы увидеть код, подобный следующему:
struct IntWrapper {
int value;
constexpr IntWrapper(int value): value{value} { }
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 перегрузок.Именно здесь в помощь нам приходит новый оператор C++20 spaceship. Давайте посмотрим, как можно написать исходный
IntWrapper
в мире C++20:#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
.Мы не только удалили 5 лишних строк, но нам даже не нужно ничего определять, компилятор сделает это за нас.
is_lt
остается неизменным и просто работает, оставаясь при этом constexpr
, хотя мы не указали это явно в нашем дефолтном operator<=>
. Это хорошо, но некоторые люди могут ломать голову над тем, почему is_lt
разрешено компилировать, даже если он вообще не использует оператор spaceship. Давайте найдем ответ на этот вопрос.Переписывание выражений
В C++20 компилятор вводится в новую концепцию, имеющую отношение к «переписанным» выражениям. Оператор spaceship, наряду с
operator==
, является одним из первых двух кандидатов, которые могут быть переписаны. Для более конкретного примера переписывания выражений давайте разберем пример, приведенный в is_lt
.Во время разрешения перегрузки компилятор будет выбирать из набора наиболее подходящих кандидатов, каждый из которых соответствует оператору, который нам нужен. Процесс отбора кандидатов изменяется очень незначительно для случая операций сравнения и операций эквивалентности, когда компилятор также должен собирать специальных переписанных и синтезированных кандидатов ([over.match.oper]/3.4).
Для нашего выражения
a < b
стандарт утверждает, что мы можем искать тип a
для operator<=>
или функции operator<=>
, которые принимают этот тип. Так делает компилятор и обнаруживает, что на самом деле тип a
содержит IntWrapper::operator<=>
. Затем компилятору разрешается использовать этот оператор и переписать выражение a < b
как (a <=> b) < 0
. Это переписанное выражение затем используется в качестве кандидата для нормального разрешения перегрузки.Вы можете спросить, почему это переписанное выражение является корректным. Правильность выражения фактически вытекает из семантики, которую обеспечивает оператор spaceship.
<=>
— это трехстороннее сравнение, которое подразумевает, что вы получаете не просто бинарный результат, но и порядок (в большинстве случаев). Если у вас есть порядок, вы можете выразить этот порядок в терминах любых операций сравнения. Быстрый пример, выражение 4 <=> 5 в C++20 вернет вам результат std::strong_ordering::less
. Результат std::strong_ordering::less
подразумевает, что 4
не только отличается от 5
но и строго меньше этого значения, что делает применение операции (4 <=> 5) < 0
правильным и точным для описания нашего результата.Используя приведенную выше информацию, компилятор может взять любой обобщенный оператор сравнения (т.е.
<
, >
, и т.д.) и переписать его в терминах оператора 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)
Это имеет смысл до версии C++20, и способ решения этой проблемы заключается в добавлении некоторых дополнительных функций
friend
в IntWrapper
, которые занимают левую сторону от int
. Если вы попробуете построить этот пример с помощью компилятора и определения IntWrapper
C++20, вы можете заметить, что он, опять же, просто работает. Давайте рассмотрим, почему приведенный выше код все еще компилируется в C++20.Во время разрешения перегрузки компилятор также будет собирать то, что стандарт называет «синтезированными» кандидатами, или переписанным выражением с обратным порядком параметров. В приведенном выше примере компилятор попытается использовать переписанное выражение
(42 <=> a) < 0
, но обнаружит, что нет преобразования из IntWrapper
в int
, чтобы удовлетворить левую часть, так что переписанное выражение отбрасывается. Компилятор также вызывает «синтезированное» выражение 0 < (a <=> 42)
и обнаруживает, что происходит преобразование из int
в IntWrapper
через его конструктор преобразования, поэтому этот кандидат используется.Цель синтезированных выражений состоит в том, чтобы избежать путаницы в необходимости написания шаблонов функций
friend
, чтобы заполнить пробелы, в которых ваш объект может быть преобразован из других типов. Синтезированные выражения обобщаются до 0 @ (b <=> a)
.Более сложные типы
Сгенерированный компилятором оператор 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
.Для борьбы с этим была статья P1185R2, в которой подробно описывается, как компилятор переписывает и генерирует
operator==
независимо от оператора spaceship. Наш IntWrapper
может быть написан следующим образом:#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!
Мы призываем вас выйти и опробовать оператор spaceship, он доступен прямо сейчас в Visual Studio 2019 под
/std:c++latest
! Как примечание, изменения, внесенные в P1185R2, будут доступны в Visual Studio 2019 версии 16.2. Пожалуйста, имейте в виду, что оператор spaceship является частью C++20 и подвержен некоторым изменениям вплоть до того момента, когда C++20 будет финализирован.Как всегда, мы ждем ваших отзывов. Не стесняйтесь присылать любые комментарии по электронной почте по адресу visualcpp@microsoft.com, через Twitter @visualc, или Facebook Microsoft Visual Cpp.
Если вы столкнулись с другими проблемами с MSVC в VS 2019, сообщите нам об этом через опцию «Сообщить о проблеме», либо из установщика, либо из самой Visual Studio IDE. Для предложений или сообщией об ошибках, пишите нам через DevComm.