О работе ракеты <=>

Вопрос о том, как правильно взлететь на ракете :)

Я об операторе <=>. Сначала мне казалось, что это будет что-то простое, вроде strcmp - возвращает меньше, больше или равно.

Первое разочарование — что возвращат какой-то "левый" тип, который даже не используешь непосредственно в if. Решил, что не очень он мне и нужен, такой странный.

Но недавно попалась заметка, что вроде бы, если определить только его для своего типа, то не нужно писать ни один оператор сравнения. Начал было искать и читать о нем, например, это - но видно, мало выпил :( Ясности не добавилось.

Может мне кто-то помочь уяснить, правильно ли я понимаю, что, определив один оператор <=>, я автоматически получаю все операторы отношений?

И как верно его определять?

Как бонус :) - почему он такой сложный? Не просто, скажем, возвращающий -1, 0, +1?

Ну, или подскажите, где есть какой-то разжеванный материал на эту тему, желательно, на русском языке.


Ответы (2 шт):

Автор решения: Harry

Как человек практический :), приведу практический же пример переопределения оператора.

Если вас интересует сравнение по полям (типа как в кортеже), то достаточно в классе (для определенности 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. Программирование — наука экспериментальная :), так что просто попробуйте поковыряться в коде...

→ Ссылка
Автор решения: HolyBlackCat

Вызывать <=> руками обычно не надо. Но если очень хочется, то его результат можно сравнивать с нулем, примерно как результат 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<), и возможно еще в чем-то.

→ Ссылка