Какой тип ordering должен возвращать мой operator в C++?

5aed854677956be2c5f3a19a4353c54d.png

На Хабре было опубликовано уже достаточно статей, посвященных «spaceship operator» operator<=> ([1], [2], [3], [4]) И этой статьи бы не было, если бы все они были идеальны и описывали его во всей полноте. Но ни одна из них в деталях не рассказывает: а какой тип, собственно, должен возвращать наш operator<=>, если мы реализуем его своими руками: std::strong_ordering, std::weak_ordering или std::partial_ordering? И какая вообще между ними разница?

Пропозал Consistent comparison (P0515) дает на этот вопрос краткий, но содержательный ответ:

  • Возвращайте _ordering, если ваш тип поддерживает <, и тогда для него будут «сгенерированы» симметричные операторы <, >, <=, >=, == и !=. Иначе возвращайте _equality, и тогда будут «сгенерированы» симметричные операторы == и !=

  • Возвращайте strong_, если для вашего типа a == b подразумевает f(a) == f(b) (то есть, если для него равенство подразумевает взаимозаменяемость; где f считывает только public const члены), иначе возвращайте weak_

И в дополнение говорит, что кроме этого существует std::partial_ordering, поддерживающий «неупорядоченные» результаты.

То же самое, но в виде таблицы (и без std::partial_ordering):

a < b должно поддерживаться?

Да: _ordering

Нет: _equality

a == b подразумевает f (a) == f (b)?

Да: strong

std::strong_ordering

std::strong_equality

Нет: weak

std::weak_ordering

std::weak_equality

NB. std::strong_equality и std::weak_equality были убраны из C++ другим пропозалом, принятым вместе с вышеупомянутым: Remove std: weak_equality and std: strong_equality (P1959). Если ваш тип поддерживает только сравнение на равенство и неравенство — вам не нужен operator<=>. Вам нужно определить только operator== (operator!= будет сгенерирован компилятором)

Разберем вышенаписанное подробнее:)

Полностью упорядоченный тип

Как узнать, что ваш тип полностью упорядоченный? Следуйте нашей инструкции!

Его эквивалентные (a == b) значения неразличимы? Он не может представлять несравнимые значения? Их вообще возможно сранивать с помощью «больше», «меньше» и «равно»? Если ваши ответы — да, да, да, то ваш тип полностью упорядоченный и его operator<=> должен возвращать std::strong_ordering.

Таким типом может быть, например, класс Person, который мы можем пожелать сортировать в первую очередь по фамилии, далее — по имени, далее — по ИНН.

class Person {
  string tax_ident;
  string first_name;
  string last_name;
public:
    std::strong_ordering operator<=>(const Person& rhs) const {
      if (auto cmp = last_name <=> rhs.last_name; cmp != 0) return cmp;
      if (auto cmp = first_name <=> rhs.first_name; cmp != 0) return cmp;
      return tax_ident <=> rhs.tax_ident;
    }
};

Слабо упорядоченные тип

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

Так, класс CaseInsensitiveString является слабо упорядоченным: хоть для него "abc" == "aBc" и возвращает true, мы не можем не согласиться, что эти значения различимы. Так что в его operator<=> более уместным и правильным будет возвращать std::weak_ordering.

class CaseInsensitiveString {
  string s;
public:
  std::weak_ordering operator<=>(const CaseInsensitiveString& rhs) const {
    return case_insensitive_compare(s.c_str(), rhs.s.c_str());
  }
};

Частично упорядоченный тип

А что, если эквивалентные значения нашего типа различимы, и, кроме того, наш тип может представлять несравнимые значения?

Например, если он представляет человека относительно некоего семейного древа: между двумя значениями такого типа можно установить отношение эквивалентности (a == b, если a и b — один и тот же человек), отношение «меньше» (a < b, если a — потомок b) и отношение «больше» (a > b, если a — предок b). Но, кроме этого, для него существуют случаи, когда значения несравнимы: например, когда a и b — разные люди, никак не связанные кровными узами.

class PersonInFamilyTree {
public:
  std::partial_ordering operator<=>(const PersonInFamilyTree& rhs) const {
    if (this->is_the_same_person_as(rhs)) return partial_ordering::equivalent;
    if (this->is_transitive_child_of(rhs)) return partial_ordering::less;
    if (rhs.is_transitive_child_of(*this)) return partial_ordering::greater;
    return partial_ordering::unordered;
  }
};

Более жизненный пример частично упорядоченных типов — float и double. Хоть в большинстве случаев мы можем установить между ними отношение (на которые обычно полагаться не стоит) эквивалентности, «меньше» и «больше», но, например, NaN не эквивалентен, не больше и не меньше чего-либо, даже другого NaN.

Теперь вы знаете, как правильно передать семантику вашего operator<=> с помощью типов std::strong_ordering, std::weak_ordering и std::partial_ordering, какой из них следует использовать в каком случае. Вы великолепны!

Опубликовано при поддержке C++ Moscow

© Habrahabr.ru