В C++20 появился оператор трехстороннего сравнения, также известный как «оператор космического корабля» из-за его внешнего вида: <=>
. Цель — упростить процесс сравнения объектов.
Основы
Ниже приведен простой пример использования этого нового оператора космического корабля:
#include <compare> int main() { int a = 1; int b = 2; auto result = a <=> b; if (result < 0) { std::cout << "a is less than b" << std::endl; } else if (result == 0) { std::cout << "a is equal to b" << std::endl; } else { // result > 0 std::cout << "a is greater than b" << std::endl; } return 0; }
Обратите внимание, что заголовок compare
должен быть включен.
Для целочисленных типов, таких как int
, тип значения, возвращаемого оператором космического корабля, — std::strong_ordering
, который может иметь одно из трех значений:
std::strong_ordering::less
: Если левый операнд (a
) меньше правого операнда (b
).std::strong_ordering::equal
: Еслиa
равноb
.std::strong_ordering::greater
: Еслиa
больше, чемb
.
Для типов с плавающей запятой, таких как double
, оператор космического корабля возвращает одно из четырех возможных значений:
std::partial_ordering::less
: Еслиa
меньшеb
.std::partial_ordering::equivalent
: Еслиa
«эквивалентно»b
. По сути, это то же самое, что и «равно», но также включает случай-0 <=> +0
.std::partial_ordering::greater
: Еслиa
большеb
.std::partial_ordering::unordered
: Ifa
orb
isNaN
.
Вы также можете напрямую использовать оператор космического корабля с некоторыми другими типами, например std::vector
и std::string
.
Объекты
Давайте сначала рассмотрим порядок в пользовательской структуре данных до C++20:
struct Foo { int value; bool operator==(const Foo& rhs) const { return value == rhs.value; } bool operator!=(const Foo& rhs) const { return !(value == rhs.value); } bool operator<(const Foo& rhs) const { return value < rhs.value; } bool operator>(const Foo& rhs) const { return value > rhs.value; } bool operator<=(const Foo& rhs) const { return value <= rhs.value; } bool operator>=(const Foo& rhs) const { return value >= rhs.value; } }; int main() { Foo a{1}; Foo b{2}; std::cout << std::boolalpha << (a == b) << std::endl; // prints false std::cout << std::boolalpha << (a != b) << std::endl; // prints true std::cout << std::boolalpha << (a < b) << std::endl; // prints true std::cout << std::boolalpha << (a > b) << std::endl; // prints false std::cout << std::boolalpha << (a <= b) << std::endl; // prints true std::cout << std::boolalpha << (a >= b) << std::endl; // prints false }
В этом примере, скомпилированном с использованием C++17, если мы не определим все операторы сравнения в структуре Foo
, компилятор будет генерировать ошибки при использовании этих отсутствующих операторов. Весь шаблонный код в C++20 можно сильно упростить с помощью оператора космического корабля:
#include <compare> struct Foo { int value; auto operator<=>(const Foo& rhs) const = default; }; int main() { Foo a{1}; Foo b{2}; std::cout << std::boolalpha << (a == b) << std::endl; // prints false std::cout << std::boolalpha << (a != b) << std::endl; // prints true std::cout << std::boolalpha << (a < b) << std::endl; // prints true std::cout << std::boolalpha << (a > b) << std::endl; // prints false std::cout << std::boolalpha << (a <= b) << std::endl; // prints true std::cout << std::boolalpha << (a >= b) << std::endl; // prints false }
Здесь следует отметить несколько вещей. Во-первых, в данном случае оператор больше не имеет возвращаемого типа bool
, а вместо этого имеет std::strong_ordering
, что может быть определено компилятором при использовании auto
. Во-вторых, в C++20 также добавлена возможность использования операторов сравнения по умолчанию, что устраняет необходимость выписывать return value <=> rhs.value;
. Однако если вы возьмете первый пример C++17 и просто замените определения операторов на default
, это не сработает! Это связано с концепцией переписывания, которая объясняется далее в этой статье.
Основные и вторичные операторы
Посмотрите на следующую таблицу из статьи Барри Ревзина Сравнения в C++20:
В C++20 есть две категории операторов: равенство и упорядочение. Оператор первичного равенства — ==
, а оператор первичного упорядочения — <=>
. Оператор вторичного равенства — !=
, а операторы вторичного упорядочения — <
, >
, <=
и >=
.
Первичные операторы всегда могут использоваться по умолчанию, и вы можете использовать по умолчанию вторичные операторы, если определен соответствующий первичный оператор. Например, следующий пример компилируется правильно:
#include <compare> struct Foo { int value; auto operator<=>(const Foo& rhs) const = default; bool operator<(const Foo& rhs) const = default; }; int main() { Foo a{1}; Foo b{2}; std::cout << std::boolalpha << (a < b) << std::endl; // prints true }
Однако если оператор космического корабля <=>
не определен, этот код не будет компилироваться:
#include <compare> struct Foo { int value; bool operator<(const Foo& rhs) const = default; }; int main() { Foo a{1}; Foo b{2}; std::cout << std::boolalpha << (a < b) << std::endl; // Object of type 'Foo' cannot be compared because its "operator<" is implicitly deleted }
В C++20 есть две новые функции, связанные с операторами сравнения:
- Основные операторы можно поменять местами. Возьмем следующий пример:
#include <compare> struct Foo { int value; explicit Foo(int value) : value(value) {} bool operator==(const int otherValue) const { return value == otherValue; } }; int main() { Foo a{10}; std::cout << std::boolalpha << (a == 10) << std::endl; // prints true }
Понятно, что a == 10
вернет true
, поскольку конструктор устанавливает a.value
в 10
, и выражение оценивается как a.operator==(10)
. Однако вплоть до C++20 выражение 10 == a
приводило к ошибке компилятора, поскольку такого оператора не было. В C++20 первичные операторы можно перевернуть. Это означает, что 10 == a
будет компилироваться, поскольку язык знает, что a == 10
и 10 == a
функционально означают одно и то же.
- Вторичные операторы можно переписать. В C++20 вторичные операторы могут быть переписаны в терминах их основного оператора. Например,
a < b
переписывается как(a <=> b) < 0
. Это то, что позволяет оператору космического корабля заменять других операторов заказа. Посмотрите на следующий пример:
#include <compare> struct Foo { int value; explicit Foo(int value) : value(value) {} auto operator<=>(const int otherValue) const { return value <=> otherValue; } }; int main() { Foo a{1}; std::cout << std::boolalpha << (a < 10) << std::endl; // prints true }
Здесь при вычислении a < 10
компилятор сначала ищет operator<
и терпит неудачу, а затем ищет переписанную версию основного оператора. Таким образом, это выражение будет оценено как a.operator<=>(10) < 0
, что позволит нам использовать <
без явного определения этого оператора в структуре Foo
.
Аналогично, тот же принцип применяется к другому основному оператору: ==
и его вторичному оператору: !=
. Для этих операторов равенства a != b
будет переписано как !(a == b)
.
Важное примечание: операторы равенства и упорядочения логически разделены, а это означает, что <=>
никогда не будет вызывать ==
и наоборот. Это означает, что вы всегда должны определять оба <=>
и ==
первичные операторы и только эти первичные операторы (вторичные операторы будут переписаны, поэтому их определять не нужно). Предостережение: если вы используете по умолчанию оператор <=>
, то оператор ==
будет неявно использоваться по умолчанию. Чтобы прояснить это, приведем несколько примеров «хорошего» и «плохого» использования операторов сравнения в C++20:
- Хорошо:
<=>
по умолчанию, что неявно устанавливает значение по умолчанию==
struct Foo { int value; auto operator<=>(const Foo& rhs) const = default; }; int main() { Foo a{1}; Foo b{2}; std::cout << std::boolalpha << (a < b) << std::endl; // No error. <=> is defaulted, so a < b gets rewritten in terms of <=> std::cout << std::boolalpha << (a == b) << std::endl; // No error. == is implicitly defaulted by defaulting <=> }
- Плохо: только дефолт
==
struct Foo { int value; bool operator==(const Foo& rhs) const = default; }; int main() { Foo a{1}; Foo b{2}; std::cout << std::boolalpha << (a < b) << std::endl; // Error. <=> is not defined in any way std::cout << std::boolalpha << (a == b) << std::endl; // No error. == is defaulted }
- Хорошо: определение
<=>
и определение==
struct Foo { int value; auto operator<=>(const Foo& rhs) const { return value <=> rhs.value; } bool operator==(const Foo& rhs) const { return value == rhs.value; } }; int main() { Foo a{1}; Foo b{2}; std::cout << std::boolalpha << (a < b) << std::endl; // No error. <=> is defined, so a < b gets rewritten in terms of <=> std::cout << std::boolalpha << (a == b) << std::endl; // No error. == is defined }
- Плохо: только определение
<=>
struct Foo { int value; auto operator<=>(const Foo& rhs) const { return value <=> rhs.value; } }; int main() { Foo a{1}; Foo b{2}; std::cout << std::boolalpha << (a < b) << std::endl; // No error. <=> is defined, so a < b gets rewritten in terms of <=> std::cout << std::boolalpha << (a == b) << std::endl; // Error. == is not defined in any way }
- Хорошо: определение
<=>
, но также использование по умолчанию==
.
struct Foo { int value; auto operator<=>(const Foo& rhs) const { return value <=> rhs.value; } bool operator==(const Foo& rhs) const = default; }; int main() { Foo a{1}; Foo b{2}; std::cout << std::boolalpha << (a < b) << std::endl; // No error. <=> is defined, so a < b gets rewritten in terms of <=> std::cout << std::boolalpha << (a == b) << std::endl; // No error. == is defaulted }
Заключение
Независимо от того, используете ли вы <=>
непосредственно для сравнений, как в первом примере, оператор космического корабля является полезной функцией для уменьшения объема шаблонного кода при определении пользовательских типов и важным дополнением к C++20, о котором должны знать современные программисты C++. Подведем итог правилам и рекомендациям:
- Оператор
<=>
возвращает тип*_ordering
, а неbool
. - Операторы равенства (
==
и!=
) и операторы упорядочения (<
,>
,<=
и>=
) логически разделены. - Первичные операторы (
==
и<=>
) можно поменять местами. - Вторичные операторы (
!=
,<
,>
,<=
и>=
) можно переписать в терминах их основного оператора. - При определении пользовательского типа вам следует просто использовать оператор
<=>
по умолчанию, не определяя какие-либо вторичные операторы. Если вы не можете использовать его по умолчанию и вам необходимо определить его более сложным способом, вам также необходимо определить/по умолчанию использовать оператор==
.