В 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: If a or b is NaN.

Вы также можете напрямую использовать оператор космического корабля с некоторыми другими типами, например 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.
  • Операторы равенства (== и !=) и операторы упорядочения (<, >, <= и >=) логически разделены.
  • Первичные операторы (== и <=>) можно поменять местами.
  • Вторичные операторы (!=, <, >, <= и >=) можно переписать в терминах их основного оператора.
  • При определении пользовательского типа вам следует просто использовать оператор <=> по умолчанию, не определяя какие-либо вторичные операторы. Если вы не можете использовать его по умолчанию и вам необходимо определить его более сложным способом, вам также необходимо определить/по умолчанию использовать оператор ==.