Новый оператор spaceship (космический корабль) в C++20

Автор оригинала: Microsoft
  • Перевод
C++20 добавляет новый оператор, названный «космическим кораблем»: <=>. Не так давно Simon Brand опубликовал пост, в котором содержалась подробная концептуальная информация о том, чем является этот оператор и для каких целей используется. Главной задачей этого поста является изучение конкретных применений «странного» нового оператора и его аналога operator==, а также формирование некоторых рекомендаций по его использованию в повседневном кодинге.


Сравнение


Нет ничего необычного в том, чтобы увидеть код, подобный следующему:

struct IntWrapper {
  int value;
  constexpr IntWrapper(int value): value{value} { }
  bool operator==(const IntWrapper& rhs) const { return value == rhs.value; }
  bool operator!=(const IntWrapper& rhs) const { return !(*this == rhs);    }
  bool operator<(const IntWrapper& rhs)  const { return value < rhs.value;  }
  bool operator<=(const IntWrapper& rhs) const { return !(rhs < *this);     }
  bool operator>(const IntWrapper& rhs)  const { return rhs < *this;        }
  bool operator>=(const IntWrapper& rhs) const { return !(*this < rhs);     }
};

Примечание: внимательные читатели заметят, что это на самом деле даже менее многословно, чем должно быть в коде до версии C++20. Подробнее об этом позже.

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

constexpr bool is_lt(const IntWrapper& a, const IntWrapper& b) {
  return a < b;
}
int main() {
  static_assert(is_lt(0, 1));
}

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

error C3615: constexpr function 'is_lt' cannot result in a constant expression

Проблема в том, что был забыт constexpr в функции сравнения. Затем некоторые добавят constexpr во все операторы сравнения. Несколько дней спустя кто-то добавит помощник is_gt, но заметит, что все операторы сравнения не имеют спецификации исключений, и придется проходить один и тот же утомительный процесс добавления noexcept к каждой из 5 перегрузок.

Именно здесь в помощь нам приходит новый оператор C++20 spaceship. Давайте посмотрим, как можно написать исходный IntWrapper в мире C++20:

#include <compare>
struct IntWrapper {
  int value;
  constexpr IntWrapper(int value): value{value} { }
  auto operator<=>(const IntWrapper&) const = default;
};

Первое отличие, которое вы можете заметить — это новое включение <compare>. Заголовок <compare> отвечает за заполнение компилятора всеми типами категорий сравнения, необходимыми для оператора spaceship, чтобы он возвращал тип, подходящий для нашей дефолтной функции. В приведенном выше фрагменте тип возвращаемого значения auto будет std::strong_ordering.

Мы не только удалили 5 лишних строк, но нам даже не нужно ничего определять, компилятор сделает это за нас. is_lt остается неизменным и просто работает, оставаясь при этом constexpr, хотя мы не указали это явно в нашем дефолтном operator<=>. Это хорошо, но некоторые люди могут ломать голову над тем, почему is_lt разрешено компилировать, даже если он вообще не использует оператор spaceship. Давайте найдем ответ на этот вопрос.

Переписывание выражений


В C++20 компилятор вводится в новую концепцию, имеющую отношение к «переписанным» выражениям. Оператор spaceship, наряду с operator==, является одним из первых двух кандидатов, которые могут быть переписаны. Для более конкретного примера переписывания выражений давайте разберем пример, приведенный в is_lt.

Во время разрешения перегрузки компилятор будет выбирать из набора наиболее подходящих кандидатов, каждый из которых соответствует оператору, который нам нужен. Процесс отбора кандидатов изменяется очень незначительно для случая операций сравнения и операций эквивалентности, когда компилятор также должен собирать специальных переписанных и синтезированных кандидатов ([over.match.oper]/3.4).

Для нашего выражения a < b стандарт утверждает, что мы можем искать тип a для operator<=> или функции operator<=>, которые принимают этот тип. Так делает компилятор и обнаруживает, что на самом деле тип a содержит IntWrapper::operator<=>. Затем компилятору разрешается использовать этот оператор и переписать выражение a < b как (a <=> b) < 0. Это переписанное выражение затем используется в качестве кандидата для нормального разрешения перегрузки.

Вы можете спросить, почему это переписанное выражение является корректным. Правильность выражения фактически вытекает из семантики, которую обеспечивает оператор spaceship. <=> — это трехстороннее сравнение, которое подразумевает, что вы получаете не просто бинарный результат, но и порядок (в большинстве случаев). Если у вас есть порядок, вы можете выразить этот порядок в терминах любых операций сравнения. Быстрый пример, выражение 4 <=> 5 в C++20 вернет вам результат std::strong_ordering::less. Результат std::strong_ordering::less подразумевает, что 4 не только отличается от 5 но и строго меньше этого значения, что делает применение операции (4 <=> 5) < 0 правильным и точным для описания нашего результата.

Используя приведенную выше информацию, компилятор может взять любой обобщенный оператор сравнения (т.е. <, >, и т.д.) и переписать его в терминах оператора spaceship. В стандарте переписанное выражение часто упоминается как (a <=> b) @ 0 где @ представляет любую операцию сравнения.

Синтезирующие выражения


Читатели, возможно, заметили тонкое упоминание «синтезированных» выражений выше, и они также играют роль в этом процессе переписывания операторов. Рассмотрим следующую функцию:

constexpr bool is_gt_42(const IntWrapper& a) {
  return 42 < a;
}

Если мы используем наше первоначальное определение для IntWrapper, этот код не будет компилироваться.

error C2677: binary '<': no global operator found which takes type 'const IntWrapper' (or there is no acceptable conversion)

Это имеет смысл до версии C++20, и способ решения этой проблемы заключается в добавлении некоторых дополнительных функций friend в IntWrapper , которые занимают левую сторону от int. Если вы попробуете построить этот пример с помощью компилятора и определения IntWrapper C++20, вы можете заметить, что он, опять же, просто работает. Давайте рассмотрим, почему приведенный выше код все еще компилируется в C++20.

Во время разрешения перегрузки компилятор также будет собирать то, что стандарт называет «синтезированными» кандидатами, или переписанным выражением с обратным порядком параметров. В приведенном выше примере компилятор попытается использовать переписанное выражение (42 <=> a) < 0, но обнаружит, что нет преобразования из IntWrapper в int, чтобы удовлетворить левую часть, так что переписанное выражение отбрасывается. Компилятор также вызывает «синтезированное» выражение 0 < (a <=> 42) и обнаруживает, что происходит преобразование из int в IntWrapper через его конструктор преобразования, поэтому этот кандидат используется.

Цель синтезированных выражений состоит в том, чтобы избежать путаницы в необходимости написания шаблонов функций friend , чтобы заполнить пробелы, в которых ваш объект может быть преобразован из других типов. Синтезированные выражения обобщаются до 0 @ (b <=> a).

Более сложные типы


Сгенерированный компилятором оператор spaceship не останавливается на отдельных членах классов, он генерирует правильный набор сравнений для всех подобъектов в ваших типах:

struct Basics {
  int i;
  char c;
  float f;
  double d;
  auto operator<=>(const Basics&) const = default;
};
 
struct Arrays {
  int ai[1];
  char ac[2];
  float af[3];
  double ad[2][2];
  auto operator<=>(const Arrays&) const = default;
};
 
struct Bases : Basics, Arrays {
  auto operator<=>(const Bases&) const = default;
};
 
int main() {
  constexpr Bases a = { { 0, 'c', 1.f, 1. },
                        { { 1 }, { 'a', 'b' }, { 1.f, 2.f, 3.f }, { { 1., 2. }, { 3., 4. } } } };
  constexpr Bases b = { { 0, 'c', 1.f, 1. },
                        { { 1 }, { 'a', 'b' }, { 1.f, 2.f, 3.f }, { { 1., 2. }, { 3., 4. } } } };
  static_assert(a == b);
  static_assert(!(a != b));
  static_assert(!(a < b));
  static_assert(a <= b);
  static_assert(!(a > b));
  static_assert(a >= b);
}

Компилятор знает, как развернуть члены классов, которые являются массивами, в их списки подобъектов и сравнить их рекурсивно. Конечно, если вы хотите написать тела этих функций самостоятельно, вы все равно получите пользу от переписывания выражений компилятором.

Выглядит как утка, плавает как утка, и крякает как operator==


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

Канонический пример со сравнением двух строк. Если у вас есть строка "foobar" и вы сравниваете ее со строкой "foo", используя ==, можно ожидать, что эта операция будет почти постоянной. Эффективный алгоритм сравнения строк следующий:

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

В соответствии с правилами оператора spaceship мы должны начать с сравнения каждого элемента, пока не найдем тот, который отличается. В нашем примере "foobar" и "foo" только при сравнении 'b' и '\0' вы наконец возвращаете false.

Для борьбы с этим была статья P1185R2, в которой подробно описывается, как компилятор переписывает и генерирует operator== независимо от оператора spaceship. Наш IntWrapper может быть написан следующим образом:

#include <compare>
struct IntWrapper {
  int value;
  constexpr IntWrapper(int value): value{value} { }
  auto operator<=>(const IntWrapper&) const = default;
  bool operator==(const IntWrapper&) const = default;
};

Еще один шаг… однако, есть хорошие новости; вам на самом деле не нужно писать код выше, потому что простого написания auto operator<=>(const IntWrapper&) const = default достаточно, чтобы компилятор неявно сгенерировал отдельный и более эффективный operator== для вас!

Компилятор применяет слегка измененное правило «перезаписи», специфичное для == и !=, где в этих операторах они переписываются в терминах operator==, а не operator<=>. Это означает, что != также выигрывает от оптимизации.

Старый код не сломается


В этот момент вы можете подумать: хорошо, если компилятору разрешено выполнять эту операцию переписывания операторов, что произойдет, если я попытаюсь перехитрить компилятор:

struct IntWrapper {
  int value;
  constexpr IntWrapper(int value): value{value} { }
  auto operator<=>(const IntWrapper&) const = default;
  bool operator<(const IntWrapper& rhs) const { return value < rhs.value; }
};
constexpr bool is_lt(const IntWrapper& a, const IntWrapper& b) {
  return a < b;
}

Ответ — ничего страшного. Модель разрешения перегрузки в C++ — арена, на которой сталкиваются все кандидаты. В этом конкретном сражении у нас их три:

  • IntWrapper::operator<(const IntWrapper& a, const IntWrapper& b)
  • IntWrapper::operator<=>(const IntWrapper& a, const IntWrapper& b)

(переписанный)

  • IntWrapper::operator<=>(const IntWrapper& b, const IntWrapper& a)

(синтезированный)

Если бы мы приняли правила разрешения перегрузки в C++17, результат этого вызова был бы неоднозначным, но правила разрешения перегрузки C++20 были изменены, чтобы компилятор мог разрешить эту ситуацию до наиболее логичной перегрузки.

Существует фаза разрешения перегрузки, когда компилятор должен выполнить серию дополнительных проходов. В C ++20 появился новый механизм, в рамках которого предпочтение отдается перегрузкам, которые не переписываются и не синтезируются, что делает нашу перегрузку IntWrapper::operator<лучшим кандидатом и разрешает неоднозначность. Этот же механизм предотвращает использование синтезированных кандидатов вместо обычных переписанных выражений.

Заключительные мысли


Оператор spaceship является желанным дополнением к C++, поскольку сможет помочь упростить ваш код и писать его меньше, а иногда меньше — лучше. Так что пристегивайтесь и управляйте космическим кораблем C++20!

Мы призываем вас выйти и опробовать оператор spaceship, он доступен прямо сейчас в Visual Studio 2019 под /std:c++latest! Как примечание, изменения, внесенные в P1185R2, будут доступны в Visual Studio 2019 версии 16.2. Пожалуйста, имейте в виду, что оператор spaceship является частью C++20 и подвержен некоторым изменениям вплоть до того момента, когда C++20 будет финализирован.

Как всегда, мы ждем ваших отзывов. Не стесняйтесь присылать любые комментарии по электронной почте по адресу visualcpp@microsoft.com, через Twitter @visualc, или Facebook Microsoft Visual Cpp.

Если вы столкнулись с другими проблемами с MSVC в VS 2019, сообщите нам об этом через опцию «Сообщить о проблеме», либо из установщика, либо из самой Visual Studio IDE. Для предложений или сообщией об ошибках, пишите нам через DevComm.
Microsoft
120,48
Microsoft — мировой лидер в области ПО и ИТ-услуг
Поделиться публикацией

Похожие публикации

Комментарии 63

    +1
    А почему нельзя сделать так, чтобы компилятор всегда по умолчанию генерировал код с дефолтным поведением для операций сравнения? Тогда строчка auto operator<=>(const Basics&) const = default; ненужна. А если нужно все-таки, чтобы сравнения были «нестандартными», то тогда и переопределять операции. Или в этом есть потаённый смысл?
      +2

      Ага. И чтобы время компиляции просто так возросло на 10% (образно говоря).

        +1
        Ну, естественно нужно учитывать то, что используются эти операции или нет.
          +3
          Они могут использоваться в другой единице компиляции, и это будет известно только во время линковки.
            0
            Ждем в C++23
          0
          А почему нельзя сделать так, чтобы компилятор всегда по умолчанию генерировал код с дефолтным поведением для операций сравнения?
          Тогда перестанут выявляться случайные ошибки/опечатки, когда программист пытается сравнить не сравнимые объекты. Можно, конечно, явно требовать запрета сравнения
          auto operator<=>(const Basics&) const = delete;
          Но это ломает совместимость.
            0
            когда программист пытается сравнить не сравнимые объекты.

            Этот момент я не совсем понял. Т.е. компилятор не сможет понять, что в аргумент функции передан другой тип? Или проблема заключается в том, что класс-наследник может иметь отличные операции сравнения?
              +1
              Проблема в том, что для многих объектов несколько бессмысленно сравнивать их лексикографически. Какое-нибудь «окно», например. Или комплексные числа.
                +3
                Нельзя сравнивать объекты, которые специально не отмечены как сравнимые.
                Например, программист сделал опечатку
                Player p1, p2;
                if (p1 > p2) Greetings(p1); else Greetings(p2);
                вместо
                if (p1.score > p2.score) Greetings(p1); else Greetings(p2);

                И компилятор её не выявил.
            0
            А можно использовать operato<=> без default? А то пока выглядит не очень. Получается, он будет выводить операторы сравнения для всех полей в классе
              0
              Разумеется, можно.
                0
                То есть, определить свои правила сравнения?
                Да, разумеется, правила перегрузки как и для прочих операторов, см.
                +5
                Уже третий заход делаю на <=> и никак не пойму как он работает, что возвращает, и вообще как он сам формируется… Будет 4-й заход))
                  +20
                  Чем дальше улучшают язык с++, тем больше он становится языком для машин, а не для людей. К моему большому сожалению, текст программ на современных плюсах все сложнее читать.
                    +1
                    поверьте, сложность парсинга с++ увеличивается быстрее, чем сложность его чтения )
                      +3

                      Парсинг плюсов уже давно алгоритмически неразрешим, куда уж сложнее.

                        0

                        Что значит алгоритмическая неразрешимость? Что в общем случае поданная на вход компилятору программа не будет гарантированно распаршена за конечное число операций?

                          0
                          Шаблоны, которые разбираются на этапе препроцессора, формируют Тьюринг-полный язык, а значит, для произвольной программы нельзя (алгоритмически) проверить, скомпилируется она когда-нибудь, или нет.
                          Правда, это не совсем парсинг плюсов, но я думаю, что 0xd34df00d имел в виду это.
                            0

                            Эм… Ну только шаблоны не разбираются на этапе препроцессора (хотя препроцессор — тоже Тьюринг-полный язык, лол).


                            Но это к парсингу таки имеет отношение, так как для того, чтобы построить сколь угодно разумное AST, вам нужно уметь инстанциировать шаблоны. Так, в этом коде


                            Foo<TemplateParams>::Bar<10> val;

                            вам нужно инстанциировать Foo<TemplateParams>, чтобы понять, Bar там шаблонный тип (и тогда это создание переменной val соответствующего типа) или же это там переменная/константа (и тогда это выражение сначала сравнивает её с 10, а потом то, что вернул operator<, сравнивает с val).

                              0

                              Да, разумеется, не на стадии препроцессора. Спать надо больше :)
                              Но вообще, понятие парсинга языка неоднозначно. Давайте просто скажем, что от исходника до объектного модуля добраться — алгоритмически неразрешимая задача. И даже проверить, доберёмся ли.

                                0
                                Ну вообще Wikipedia хотя и добавляет слова «обычно», но в целом согласна, что Результатом обычно является дерево разбора (синтаксическое дерево).

                                Так что препроцессор — это, всё-таки, ещё не парсер. А фишка в том, что для большинства языков это если не простой LR(1) парсер, то что-нибудь недалеко от него ушедшее.

                                В случае же с C++ парсер обязан включать в себя интерпретатор приличного подмножества всего языка C++!

                                И туда вы можете любую задачу засунуть не только теоретически, но и практически — то есть это не тьюринговская трясина, а практически используемые вещи. Вплоть до того, что есть даже пропозал научить C++ читать ещё и внешние файлы.

                                Вот тогда совсем хорошо будет, когда ваша IDE будут ходить в prod за описанием базы данных…
                                  0

                                  У меня есть практический опыт работы с подобной ерундой, правда, не на C++. Благо, в хаскеле уже давно есть Template Haskell с произвольным codegen'ом на этапе компиляции, включая произвольные IO-действия (то есть, на прод за описанием БД или схемы сервиса ходить таки можно). И ничего, нормально всё работает. Правда, за счёт того, что на прод ходит и описание схемы или БД генерирует один модуль, а пользоваться сгенерированным можно только из другого модуля, всё становится несколько проще — не надо на каждый чих делать IO снова и снова.

                                    +1
                                    Правда, за счёт того, что на прод ходит и описание схемы или БД генерирует один модуль, а пользоваться сгенерированным можно только из другого модуля
                                    Не, это читерство. Так и rust умеет. И думаю много кто ещё. А вот по настоящему, по хардкорному, чтобы если компилируют в 10000 потоков на сервере компиляции, то весь прод вставал бы раком — это только в C++. И то пока не включили в стандарт.
                                0
                                Препроцессор С++ тоже стал обладать полнотой по Тьюрингу? Вроде же еще пара стандартов назад такого не было… Только шаблоны были тьюринг-полными.
                                  0

                                  Почти. По крайней мере, полезные циклы с boost.pp можно писать (да и без него можно, но сложно).


                                  С другой стороны, технически и шаблоны C++ не Тьюринг-полны — стандарт позволяет иметь реализациям ограничения на глубину и количество инстанциирований, например. Но тогда и компьютеры не очень Тьюринг-полны, число состояний-то тоже конечно, но про это почему-то забывают.

                                    0
                                    С другой стороны, технически и шаблоны C++ не Тьюринг-полны — стандарт позволяет иметь реализациям ограничения на глубину и количество инстанциирований, например.
                                    Ну эта проблема уже давно решена: вы можете просто завести constexpr функцию, внутри которой заводите себе массив на мегабайт (или гигабайт) и, спасибо C++14, его можно спокойно обрабатывать разными способами. Вполне себе бейсик такой — а на нём, в своё время, чего только не писали.
                                      +1

                                      ЕМНИП реализация может иметь ограничения и на сложность вычисляемых constexpr-функций.

                                        0
                                        Может, конечно. Но тут уже начинаются проблемы с компилированием вполне легального кода. Стандартного значение -fconstexpr-steps=1048576 достаточно, чтобы компиляция занимала часы.
                        0
                        del
                      0

                      Он возвращает только одно из трех значений? Как обстоят дела с "несравниваемостью"?
                      Например в D существует целая группа операторов сравнения, учитывающая возможность несравнимости, в частности для всяких NaN.

                        +6
                        На cppreference описаны 5 типов, которые может возвращать operator <=>. Один из них (std::partial_ordering) как раз учитывает возможность несравнимости.
                        +4
                        помимо этого, возможен некоторый буст производительности для алгоритмов. Например, set при лукапе будет использовать эквивалентность вместо равенства (a эквивалентен b если !(a < b) && !(b < a)), что не очень эффективно, т.к. строки сравниваются дважды даже если достаточно одного strcmp. С operator <=> некоторые алгоритмы сортировок/бинарного поиска можно оптимизировать.
                          +2
                          newtype IntWrapper = IntWrapper Int deriving (Eq, Ord, Show)

                          Я смотрю, deriving Ord спустя пару десятков лет в мэйнстрим завезли. deriving Show (чтоб cout << IntWrapper(42) тоже "просто заработало") пока не обещают?

                            +3

                            Не обещают, но обещают template haskell метаклассы и компилтайм-рефлексию, можно будет написать самому.


                            Ждём, когда завезут deriving via.

                            0
                            А почему нельзя было это все добавить без нового оператора? Любой знак сравнения можно вывести из любого.
                              0
                              Далеко не всё можно так вывести.
                              Пример гарантированно эффективно раскладывающегося в железо кода
                              /* MAC address. */
                              #pragma pack(push, 1)
                              typedef struct lnk_mac_t
                              {
                              	union
                              	{
                              		uint8_t mac[6];
                              		uint32_t _f32tv;
                              		uint16_t _f16tv[3];
                              	};
                              #if defined(__cplusplus)
                              	inline const uint32_t& _first_part_get() const
                              	{
                              		return _f32tv;
                              	}
                              	inline const uint16_t& _second_part_get() const
                              	{
                              		return _f16tv[2];
                              	}
                              	inline operator const uint8_t* () const
                              	{
                              		return static_cast<const uint8_t*>(mac);
                              	}
                              	inline bool operator < (const lnk_mac_t& p) const
                              	{
                              		if (_first_part_get() < p._first_part_get())
                              			return true;
                              
                              		if (_second_part_get() < p._second_part_get())
                              			return true;
                              
                              		return false;
                              	}
                              	inline bool operator > (const lnk_mac_t& p) const
                              	{
                              		if (_first_part_get() > p._first_part_get())
                              			return true;
                              
                              		if (_second_part_get() > p._second_part_get())
                              			return true;
                              
                              		return false;
                              	}
                              	inline bool operator == (const lnk_mac_t& p) const
                              	{
                              		if (_first_part_get() != p._first_part_get())
                              			return false;
                              
                              		if (_second_part_get() != p._second_part_get())
                              			return false;
                              
                              		return true;
                              	}
                              	inline bool operator != (const lnk_mac_t& p) const
                              	{
                              		if (_first_part_get() != p._first_part_get())
                              			return true;
                              
                              		if (_second_part_get() != p._second_part_get())
                              			return true;
                              
                              		return false;
                              	}
                              	inline bool isIndividual() const
                              	{
                              		return ((mac[0] &
                              #if __BYTE_ORDER == __LITTLE_ENDIAN
                              			0x01
                              #elif __BYTE_ORDER == __BIG_ENDIAN
                              			0x80
                              #else
                              # error	"Please fix"
                              #endif
                              			) == 0); // See RFC 2469 for details
                              	}
                              	inline bool isGroup() const
                              	{
                              		return !isIndividual();
                              	}
                              	/* TODO CTP
                              	inline bool isCTP() const
                              	{
                              		// See https://en.wikipedia.org/wiki/Ethernet_Configuration_Testing_Protocol 
                              		// See https://aminems.github.io/ctp.html
                              		// See https://habr.com/ru/post/129399/
                              		// cf:00:00:00:00:00
                              		if (_first_part_get() != 0x000000cf)
                              			return false;
                              
                              		if (_second_part_get() != 0x0000)
                              			return false;
                              
                              		return true;
                              	}
                              	*/
                              	
                              #endif // defined(__cplusplus)
                              } lnk_mac_t;
                              #pragma pack(pop)
                              


                              Если для примера или любого кода где оператор сравнения делает последовательно несколько шагов (в статье описан пример с std::string) начать из оператора less выводить остальные то получится плохо, особенно для операторов == и !=. Оператор <=> призван в перспективе, я надеюсь что в 20 успеется, решить эту проблему.
                                +1
                                Я сейчай напишу лучшее решение, и перед этим я хочу указать 2 важных момента контекста
                                1) мы обсуждаем улучшения языка (синтаксический сахар для уменьшения кода) и компилятора (улучшение производительности)
                                2) все примеры имеют общую особенность — последовательность сравнений битовый строк (числа, строки). Придумать другой пример мне тоже не получилось.

                                Лучше вместо нового оператора создать магический метод сравнения. Который возвращает последовательность битовых сущностей.
                                inline const __comparation_sequence()
                                {
                                    return {_first_part_get(), _second_part_get()}
                                }
                                


                                Компилятору будет просто сгененировать все операции сравнения на основе такой последовательности
                                  0
                                  Да, очень хорошее предложение. Именно явно указанная последовательность членов если это необходимо и использование, например, ключевого слова auto чтобы компилятор сам эту последовательность выстроил.
                                    0
                                    Нельзя всё сводить к битовым строкам.
                                    Вполне можно представить себе кастомную функцию сравнения для сортировки, которая выполняет обычное сравнение строк, но не различает буквы Е и Ё.
                                      0
                                      Все известные мне решения используют нормализацию. То есть мы опять получаем банальную последовательность битов. Отличаются только оптимизации. В джаваскрипте приведут всю строку к нормализованной. В си будет указатель на букву каждой строки, который нормализует только букву и только если дойдет до нее.

                                      В моей идее — будет использована не сама переменная структуры, а функция от нее.
                                        +1

                                        А зачем? И вы уверены, что вы хотите всегда резво (ээ, какой каноничный аналог eagerly как антонима lazily?) строить репрезентативную битовую последовательность для всех подобъектов, когда сравниваете объекты, их содержащие?


                                        Если вы сравниваете две строки, а у них длины разные, то на байтики с самими строками вам даже смотреть не нужно.

                                          0
                                          Ну так для строк эта последовательность будет начинаться с длины.

                                          И я не совсем понимаю первый вопрос. Но мне кажется недопонимание в том, что последовательность действительно является ленивой. Компилятор получил инструкцию как ее строить. Ему уже решать построить ее сразу при создании обьекта, или начать строить в момент сравнния. Ну и в момент нахождения различия — перестать строить.
                                            0
                                            Вы начали с «магического метода». Какие в нём разрешены операторы — всё множество языка или ограниченное подмножество? Он выполняется в runtime или compile-time?
                                              0
                                              Я себе представляю это как все множество языка и компайл тайм.

                                              Естественно есть возможность переопределить операторы старым способом. Если новый не подходит или не нравится.

                                              Однако я не разработчик языка. И не тяну на уровень автора пропозала
                                                0
                                                Если compile-time, процесс сравнения не сможет зависеть от данных.
                                                Например, в зависимости от какого-нибудь флага в объекте сравнивать либо по одному полю, либо по другому. Пригодится, например, для std::string: если строка короткая, применено short string optimization и содержимое строки лежит в самом классе, иначе искать его надо по указателю.
                                                  0
                                                  Смотрите.

                                                  Есть опрератор сравнения (spaship <=>). Его уже приняли в стандарт 20, так что я буду говорить что есть. Он точно статически преобразовывает a < b в 0 < (a <=> b).

                                                  Получится ли статически оптимизировать сам <=> — вопрос к компилятору. В примерах выше — дума да. В примере с флагами — не знаю, хотя блин представить рельную проблему с таким решением сложно.

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

                                                  Теперь сравнение с моим предложением. Многие вещи напоминают оператор сравнения. Тоже есть крайние случаи где они не применимы, надо ручками. Тоже есть статическая генерация каждого оператора сравнения из единого метода. Конечный асемблерный код одинаков — прерывание на первом не одинаковом байте.

                                                  Теперь о различиях.
                                                  Кода заметно меньше
                                                  Spoiler header
                                                  Spaship
                                                  fucntion operator<=>(that) {
                                                  if (this.a <=> that.a != 0) {
                                                  return this.a <=> that.a;
                                                  }
                                                  if (this.b <=> that.b != 0) {
                                                  return this.b <=> that.b;
                                                  }

                                                  return 0;
                                                  }


                                                  Последовательность
                                                  function __compare_sequence () {
                                                  return {this.a, this.b}
                                                  }


                                                  Оператору проще оптимизировать последовательность байтов, чем последовательность ифов
                                                    0
                                                    Допустим, у меня такой класс:
                                                    struct data {
                                                        char* name;
                                                    };
                                                    Что будет обозначать
                                                    function __compare_sequence () {
                                                        return {this.name};
                                                    }
                                                    Сравнение указателей? Сравнение строк как null-terminated? А почему как null-terminated, а не фиксированной длины? А как перейти на сравнение указателей, если надо сравнивать так?
                                                      0
                                                      Отличный пример, который говорит о том, что над синтаксисом моего предложения надо работать.

                                                      Я бы предложил, что в написанном случае будут сравниваться указатели, так как `this.name` указатель.

                                                      А для распаковки указателя возможно что-то такое:
                                                      `for (...) {std::cpm::yield(char);}`.
                                                        0
                                                        Этот for находится внутри __compare_sequence?
                                                        В этом случае он не сможет работать в compile-time, т.к. содержимое строки ещё не известно.
                                                        Если же он возвращает алгоритм, который будет скомпилирован в такой цикл, то проще алгоритм написать сразу в обычном operator<=>
                                                        Для очень простого примера получается непростое решение.
                                                          0
                                                          > В этом случае он не сможет работать в compile-time, т.к. содержимое строки ещё не известно.

                                                          Для строки неизвестной длины никоке решение никакой компилятор не сможет комайл тайм.

                                                          > Если же он возвращает алгоритм, который будет скомпилирован в такой цикл, то проще алгоритм написать сразу в обычном operator<=>

                                                          Это субьективно. Лично мне проще работать с данными только текущего обьекта и не добавлять работу со вторым обьектом.
                                                          Более того можно сомастоятельно дописать (и в будущем добавить в std) что-то вроде `yield_from(char* name, int name_len)`

                                                          > Для очень простого примера получается непростое решение

                                                          Опять же субьективно. Если я пишу на сях, то в голове держу граф ссылок и подсчет тактов. Я согласен, что сама моя идея «если в процессоре все равно сравнить можно только инты => то давайте в языке сразу их перечислим» не проста. Но заниматся оптимизацией на уровне нескольких тактов — не может быть просто по определению. Именно поэтому ни я, ни авторы <=>, не предлагают выбрасывать существующие способы. Или как либо ограничивают написание неэффективных способов.
                                  +4
                                  Любой знак сравнения можно вывести из любого.

                                  Это утверждение верно только для частичных строго упорядоченных множеств.
                                    0
                                    А примеры только такие.

                                    Честно говоря, придумать пример программы где нужны операции сравнения не для частичных строго упорядоченных множеств я не могу.
                                      0
                                      И, тем не менее, в Javascript и PHP все операторы сравнения — именно такие.

                                      В C/C++, на самом деле тоже — но тут ограниченная проблема: разработчики IEEE 754 свинью подложили.
                                        0
                                        У меня было такое, когда пришлось сравнивать диапазоны на числовой прямой: пример типичной операции сравнения «A <= B»: существует число, принадлежащее диапазону A, такое, что все числа принадлежащие диапазону B не меньше его. В этом случае операции равенства и неравенства диапазонов не выводятся из операций сравнения. Да и вообще, операции сравнения в этом случае ни коммутативны, ни антикоммутативны, т.е. нарушается базовое условие из определения частичных упорядоченных множеств. Хотя сравнивать в прикладном смысле можно.
                                          0
                                          Хочу замететить, что в этом примере новый оператор <=> невозможно применить.

                                          То есть это действительно, это пример который опровергает мое утверждение «Любой знак сравнения можно вывести из любого». Однако если его дополнить контекстом, мое утверждение все еще верно.

                                          > Любой знак сравнения можно вывести из любого [если можно определить оператор <=>]

                                          А можно узнать, о чем была программа?
                                            0
                                            можно узнать, о чем была программа?

                                            Один из модулей ui, отвечавший за отрисовку диаграммы Гантта

                                    +3

                                    Ещё стоит отметить, что именно defaultовый operator spaceship позволяет использовать класс в качестве non-type template parameter (если у всех его полей тоже все операторы дефолтовые), так как линкер тогда знает, как удалять дублируемые символы.


                                    То есть, можно писать вещи типа


                                    struct MyTemplateParams
                                    {
                                       ...
                                       auto operator<=>(const MyTemplateParams&) const = default;
                                    };
                                    
                                    template<MyTemplateParams Params>
                                    ...

                                    что полезно при метапрограммировании.

                                      0

                                      Плюсы все сложнее и сложнее…

                                        0

                                        А в чем усложнение? В том, что не надо будет, как сейчас, писать кучу member operator'ов и friend operator'ов на каждый чих? Это упрощение, а не усложнение.

                                          +3
                                          Вы как бы оба правы. Язык — становится сложнее. Программы — на нём написанные — проще.

                                          Тут нет противоречия…
                                            0
                                            опять же, вопрос трактования. Что такое «сложность языка»? Число именованных сущностей? Букв в стандарте? Строк кода в компиляторе? На мой взгляд, за «простоту» языка стоит принимать именно выразительность/читаемость. А она с новыми стандартами только растет.
                                              0

                                              … пока вам не нужно вдруг прочесть старый код.

                                        0
                                        У Саттера была очень понятная лекция про этот оператор. Гораздо понятнее, чем эта статья.
                                        www.youtube.com/watch?v=ULkwKsag0Yk

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

                                        Самое читаемое