О работе ракеты <=>
Вопрос о том, как правильно взлететь на ракете :)
Я об операторе <=>. Сначала мне казалось, что это будет что-то простое, вроде strcmp - возвращает меньше, больше или равно.
Первое разочарование — что возвращат какой-то "левый" тип, который даже не используешь непосредственно в if. Решил, что не очень он мне и нужен, такой странный.
Но недавно попалась заметка, что вроде бы, если определить только его для своего типа, то не нужно писать ни один оператор сравнения. Начал было искать и читать о нем, например, это - но видно, мало выпил :( Ясности не добавилось.
Может мне кто-то помочь уяснить, правильно ли я понимаю, что, определив один оператор <=>, я автоматически получаю все операторы отношений?
И как верно его определять?
Как бонус :) - почему он такой сложный? Не просто, скажем, возвращающий -1, 0, +1?
Ну, или подскажите, где есть какой-то разжеванный материал на эту тему, желательно, на русском языке.
Ответы (2 шт):
Как человек практический :), приведу практический же пример переопределения оператора.
Если вас интересует сравнение по полям (типа как в кортеже), то достаточно в классе (для определенности Test) написать
auto operator<=>(const Test& t) const = default;
А если хочется странного, то надо определить этот оператор и оператор ==.
Например, вот такой класс и сравнение, где четные меньше нечетных.
class Test {
public:
Test(int x):val_(x){}
auto operator<=>(const Test& t) const
{
// Четные меньше нечетных
if (val_%2 < t.val_%2) return -1;
else if (val_%2 > t.val_%2) return -1;
else if (val_ < t.val_) return -1;
else if (val_ > t.val_) return 1;
else return 0;
}
auto operator==(const Test& t) const { return (*this <=> t) == 0; }
int val_ = 0;
};
int main(int argc, char * argv[])
{
Test a(8), b(7);
#define OUT(a,b) \
cout << a.val_ << " < " << b.val_ << " = " << (a < b) << endl; \
cout << a.val_ << " <= " << b.val_ << " = " << (a <= b) << endl; \
cout << a.val_ << " > " << b.val_ << " = " << (a > b) << endl; \
cout << a.val_ << " >= " << b.val_ << " = " << (a >= b) << endl; \
cout << a.val_ << " == " << b.val_ << " = " << (a == b) << endl; \
cout << a.val_ << " != " << b.val_ << " = " << (a != b) << endl;
OUT(a,b);
a = 7;
OUT(a,b);
b = 9;
OUT(a,b);
}
В VC++2019 выводит
8 < 7 = 1
8 <= 7 = 1
8 > 7 = 0
8 >= 7 = 0
8 == 7 = 0
8 != 7 = 1
7 < 7 = 0
7 <= 7 = 1
7 > 7 = 0
7 >= 7 = 1
7 == 7 = 1
7 != 7 = 0
7 < 9 = 1
7 <= 9 = 1
7 > 9 = 0
7 >= 9 = 0
7 == 9 = 0
7 != 9 = 1
Ну, а использовать ракету в программе можно, например, так:
if (auto cmp = (a<=>b); cmp < 0)
cout << "less";
else if (cmp > 0)
cout << "more";
else
cout << "equal";
P.S. При этом никто не мешает вам определить свои операторы отношений, в этом случае, понятно, будут использоваться именно они, а не сгенерированные.
P.P.S. Программирование — наука экспериментальная :), так что просто попробуйте поковыряться в коде...
Вызывать <=> руками обычно не надо. Но если очень хочется, то его результат можно сравнивать с нулем, примерно как результат strcmp. Пример: (a <=> b) < 0 означает a < b (скобки можно не писать). Про его возвращаемый тип ниже.
Если перегрузить <=>, написав = default, то автоматически будут работать все 6 операторов: ==, !=, <, <=, >, >=. Сравниваться будут все поля.
Перегружать с = default можно только в теле класса. На выбор:
auto operator<=>(const MyClass &) const = default;
friend auto operator<=>(const MyClass &, const MyClass &) = default;
Если перегрузить <=> руками, то будут работать только 4: <, <=, >, >=. Перегрузить == нужно отдельно, и тогда != начнет работать автоматически (это работает независимо от <=>).
Без = default можно перегружать и внутри, и снаружи класса.
Проще всего сжульничать, и написать так:
friend auto operator<=>(const MyClass &a, const MyClass &b)
{
return std::tie(a.x, a.y, a.z) <=> std::tie(b.x, b.y, b.z);
};
friend bool operator==(const MyClass &a, const MyClass &b)
{
return std::tie(a.x, a.y, a.z) == std::tie(b.x, b.y, b.z);
};
А лучше вынести tie в метод, чтобы не писать его 4 раза.
Важно не поддаться искушению и не реализовать operator== через a <=> b == 0. В общем случае это будет работать медленнее, чем сравнение полей через ==. (Например, чтобы сравнить две строки через <=>, нужен цикл по символам. А == может сначала сравнить размеры, и если они разные, больше ничего не делать.)
operator== в C++20 тоже можно перегрузить как = default. Он будет сравнивать на равенство все поля, и не будет использовать a <=> b == 0, даже если вы перегрузили <=>.
Если хочется писать <=> руками без tie, то нужно выбрать возвращаемый тип. В соседнем ответе предложили int, и он вроде даже работает, но это как-то сомнительно. На выбор есть три стандартных класса, которые ведут себя примерно как enum-ы:
std::strong_ordering- больше/меньше/равноstd::weak_ordering- больше/меньше/эквивалентно - по сути то же самое, но сигнализирует, что разные объекты могут считаться эквивалентными. Пример - сравнение строк без учета регистра.std::partial_ordering- больше/меньше/эквивалентно/несравнимо - то же чтоweak_ordering, но добавлено четвертое значение "несравнимо", которое заставляет все 6 операторов возвращать false. Пример: NaN "несравним" с любым другим числом, и с самим собой тоже.
Все три можно сравнивать с нулем: x < 0 означает x == ...::less, и т.п.
В <compare> есть еще всякие примочки для удобного написания <=>, вроде std::common_comparison_category, который подбирает подходящий возвращаемый тип по типам сравниваемых объектов.
<=> можно использовать, чтобы сравнивать объекты разных типов. Очевидно, = default в этом случае не поможет.
Компилятор понимает, что порядок операндов не важен, поэтому достаточно перегрузить A <=> B, а B <=> A заработает само, и все 4 оператора тоже будут работать с любым порядком аргументов.
Аналогично, если перегрузить A == B, то B == A будет работать сам, и != с любым порядком аргументов тоже.
Деталь: На самом деле <=> и == не генерируют никакие другие операторы. Наоборот, при попытке вызвать другой оператор, компилятор подменяет его на вызов <=> или ==, возможно меняя порядок аргументов.
Разница в том, что, например, нельзя взять адрес такого несуществующего оператора (а-ля &MyClass::operator<), и возможно еще в чем-то.