На Хабре было опубликовано уже достаточно статей, посвященных «spaceship operator» operator<=>
([1], [2], [3], [4]) И этой статьи бы не было, если бы все они были идеальны и описывали его во всей полноте. Но ни одна из них в деталях не рассказывает: а какой тип, собственно, должен возвращать наш operator<=>
, если мы реализуем его своими руками: std::strong_ordering
, std::weak_ordering
или std::partial_ordering
? И какая вообще между ними разница?
Ответ для нетерпеливых:
Возвращайте
strong_ordering
, если для вашего типаa == b
подразумеваетf(a) == f(b)
(гдеf
читает только значимое для сравнения состояние, доступное через публичные константные члены). То есть, если пользователь никак не может отличить значения, эквивалентные с точки зренияoperator==.
Возвращайте
weak_ordering
, если может быть так, что хотьa == b
и далоtrue
, пользователь может отличитьa
отb
.Возвращайте
partial_ordering
, если некоторые значения вашего типа вообще невозможно как-либо адекватно сравнивать с другими его значениями.
Остались вопросы? Разберем вышенаписанное подробнее!
Полностью упорядоченные типы
Как определить, что ваш тип является полностью упорядоченным? Следуйте нашей инструкции!
Его эквивалентные (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
, эти значения различимы, так как пользователь может отличить "abc"
от "aBc"
, сравнив значения, возвращенные публичным методом str
. Так что в его 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());
}
std::string_view str() const { return s; }
};
Подробнее о «различимости»
Под «различимостью» в контексте operator<=>
понимается именно то, может ли пользователь отличить a
от b
при том, что a == b
вернуло true
.
Так, в примере с строго упорядоченным типом Person
, пользователь никак не может отличить a
от b
, если a == b
, потому что хоть сравни он попарно все их члены (tax_ident
, first_name
и last_name
; для которых можно написать геттеры)
— он не найдет между ними разницы.
И, наоборот, тип CaseInsensitiveString
(далее — CIS
) называется слабо упорядоченным, так как пользователь может наблюдать разницу между значениями, одинаковыми с точки зрения operator==: CIS{"abc"}
(объект a
) == CIS{"aBc"}
(объект b
), но a.str() != b.str()
. Если бы CIS
не предоставлял пользователю никакой возможности получить значение исходной строки, то формально он считался бы строго упорядоченным.
Частично упорядоченные типы
А что, если эквивалентные значения нашего типа различимы, и, кроме того, наш тип может представлять несравнимые значения?
Например, если он представляет человека относительно некоего семейного древа: между двумя значениями такого типа можно установить отношение эквивалентности (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;
}
};
Более жизненный пример частично упорядоченного типа — double
(или же float)
. Хоть в большинстве случаев мы можем установить между его значениями отношение (на которые обычно полагаться не стоит) эквивалентности, «меньше» и «больше», но, например, NaN
не эквивалентен, не больше и не меньше чего-либо, даже другого NaN
.
Теперь вы знаете, как правильно передать семантику вашего operator<=>
с помощью типов std::strong_ordering
, std::weak_ordering
и std::partial_ordering
, какой из них следует использовать в каком случае. Вы великолепны!
Опубликовано при поддержке C++ Moscow