Comments 55
Выражение a == b никогда не будет вычисляться как operator<=>(a, b) == 0 неявно
имо зря убрали
В начале статьи неточность, но ниже говорится что на самом деле будет использоваться operator<=> «Это единственный случай, когда компилятор сгенерирует оператор сравнения, который вы сами не писали.»
Идея полезная, но синтаксис не просто режет глаз, это скорее штопор, в этот самый глаз вкручиваемый.
А как вам такое?
https://habr.com/ru/post/258811/
medium.com/@albert.s.chun/ruby-the-spaceship-operator-101-717b42566971
struct A {
T t;
U u;
V v;
T, U и V — это где-то в другом месте определенные типы данных? Очень уж давно учил, но подобный код встречал для идеи функции с жуткой рекурсией для определения «очень большого числа».
class Eq a where
(==) :: a -> a -> Bool
(/=) :: a -> a -> Bool
x == y = not (x /= y)
x /= y = not (x == y)
Где достаточно определить либо равенство, либо неравенство, и второе будет вычислено как не-первое.
Операции сравнения:
class (Eq a) => Ord a where
compare, (<), (<=), (>=), (>) :: a -> a -> Bool
max, min :: a -> a -> a
Где минимально и достаточно определить лишь функции compare и (<=)
В известном смысле оно не урезанное, а улучшенное (и на самом деле не из Хаскелла, а из математики). Улучшенное потому, что как раз в Хаскелле (в стандартном Prelude) нет аналога partial_ordering
.
Нельзя. Ord
— это класс линейно упорядоченных типов и реализация обязана быть консистентной и не нарушать соответствующих свойств, в частности forall a b. (Ord a, Ord b) => a <= b || b <= a
. Для NaN
соответствующее свойство нарушается, и из-за этого, например, алгоритм сортировки списка выдаёт непредсказуемый результат на списке, содержащем NaN
.
Да почему же только из Хаскелля. В том же Rust, насколько я помню, чуть ли не с самого начала есть трейты PartialEq
/Eq
и PartialOrd
/Ord
… Правда, никакого аналога weak_ordering я там не помню (если вернул Ordering::Equal
, значит, значения равны), это да.
Наш код можно ещё упростить, если категорию сравнения определять автоматически:
И какая же категория будет выведена автоматически?
Невероятно, но это тот самый случай, когда я начинал читать статью с мыслями "не надо трогать мой любимый С++, и без космических кораблей жилось хорошо", а закончил с мыслями "как я раньше без этого жил" :)
Интересно, что про spaceship говорили еще после С++14 вроде, но тогда по прочтении я еще не пробовал функциональные языки вроде Haskell или даже Rust. Теперь понять смысл почему это так удобно намного проще.
Кажется и правда функциональщина мозги вправляет) Или просто шишек себе уже набил за эти годы обо все эти операторы)
Кто писал операторы <, >, = так и дальше смогут их писать, для них ничего не изменилось.
А единый оператор <=> позволяет компилятору вывести все остальные автоматически.
— Ладно, не заливай! Ни разу не писал оператор трехстороннего сравнения?!
— Не довелось как-то, не вышло…
— Ну ты даешь! Не знал, что на конференциях и даже на хабре никуда без этого?! Пойми. Там только и говорят что про оператор "<=>". Как он бесконечно прекрасен. О коде, который они видели. О том, как код, словно по волшебству, становится короче, делая то же самое, что стало легче сравнивать вещественные числа и строки, без учёта регистра. А ты? Что ты им скажешь? Там тебя окрестят лохом!..
Классический пример — числа с плавающей запятой, для которых любая из операций 1.f < NaN, 1.f == NaN и 1.f > NaN даёт ложь. Поэтому 1.f <= NaN также даёт ложь, но при этом !(NaN < 1.f) — правда.
Мы ж на Хабре, и вроде как не для 1С-ников пишем, зачем true
и false
переводить-то?..
Раньше я реализовывал оператор скажем < следующим образом:
if (a.length()<b.length())
return true;
… а здесь более сложная логика, когда все таки длины у них одинаковые и надо уже идти проверять что там внутри.
Таким образом оператор < в подавляющем количестве вызовов был очень быстрым, т.к. тупо сводился к проверке длины.
Теперь мне говорят — для упрощения кода, ты можешь определить spaceship и
ВСЕГДА будет вызываться условный compare при сравнении.
То есть оператор < автоматически сегенерируется как:
return a.compare(b) < 0;
Эффективный код? Забудьте. compare(spaceship) сделает все возможные проверки, чтобы точно рассказать о том, какие отношения у двух переменных. Даже если вам это не надо.
Но ведь никто не запрещает определить оператор < и не оставлять его генератору!!!
Ага. Поэтому люди которые рассказывают какой классный spaceship — почему-то приводят в пример как раз строки и другие сложные структуры.
Как плюс выставляя тот факт, что теперь не надо писать сложные проверки а можно все делать через один оператор. То есть даже сами эксперты умалчивают о проблеме избыточности оператора, даже там где это актуально.
Второй момент — для кого нужен этот оператор?
Для тех, кто пишет много не типичных классов с переопределенными операторами. то есть 99% пользователей этой фичи — писатели библиотек. И уж они то будут пользоваться по полной, чтобы оптимизировать время написания кода.
Поздравляю, с внедрением spaceship библиотеки которыми вы пользуетесь станут еще медленней.
UPD: для минусующих карму.
Каждый раз, когда я высказываюсь с критикой изменений в С++ мне приходят минусовать карму.
Господа минусующие — вы идиоты. Чтобы указать на неверность критики — надо минусовать комментарий и комментировать контраргументами. Это видно и это показывает ваше отношение к конкретному высказыванию. Минусуя карму вы просто затыкаете критика.
Критика(даже глупая) не вредит стандарту, потому что специалист видит несостоятельность утверждения и игонрирует критику. А полезная критика заставляет задуматься и изменить что-то если надо.
Минусуя карму в такой ситуации вы просто увеличиваете вероятность, что проблема не будет замечена, потому что тот кто ей видит(или считает что видит) — предпочтет промолчать, зная что придут фантики которые его будут сливать.
Так и в spaceship можно точно так же сразу первой строчкой проверить length и сразу тут же и вернуть XXX_ordering::less, почему обязательно "все возможные проверки" — то?
Давайте чуть более абстрактно.
Есть две дорогих проверки первая проверка
A > B
вторая проверка
A < B
Если я делаю отдельные операции > и <, то каждая из них будет содержать только одну дорогую проверку, нужную в конкретном случае.
Если же я буду делать через spaceship — не получится для обеих операторов сделать дешево.
Либо при сравнении A > B будет сначала идти ненужное
if (A< B) return XXX_ordering::less;
а только потом сработающее
if (A > B) return XXX_ordering::greater;
либо наоборот.
Суть в том, что spaceship не знает и не может знать, какие проверки нам нужно делать в конкретном случае, а какие нет. Это общая большая функция вычисляющая отношение между переменные в деталях, даже если нам эти детали не нужны.
P.S. в общем-то и до spaceship'а обычно реализовывали всего два оператора — "==" и "<", а все остальные выражали через них путем вызова этих двух.
P.P.S.
if (A< B) return XXX_ordering::less;
if (A > B) return XXX_ordering::greater;
Почему не return A <=> B?
Внутри operator <=> тоже можно и нужно сравнивать члены структуры или класса в свою очередь через их operator <=>. Ну, это так, детали.
Сначала нужно провести определенные вычисления.
Ок, я пока помолчу и сделаю пример, раз вы так настойчиво не воспринимаете идею на абстрактных примерах.
Да, это было бы хорошо, чтобы, так сказать, в цифрах вы подкрепили как-то свои опасения. Я думаю, что если бы в std из-за этой замены будет реальная регрессия в плане производительности, это будет замечено на тестах.
Ну и в std простые типы, там не требуется делать никаких вычислений для того чтобы провести сравнение.
Ну да, и я к тому же. У вас же были опасения что вот мол писатели библиотек будут это использовать и будет медленно, std — это библиотека, и вряд ли там будет какая-то регрессия из-за применения этого оператора. А в сложных ситуациях можно написать каждый оператор отдельно, это уже up to автор библиотеки. Если у автора затруднения с написанием эффективного кода, то operator <=> — не единственное место, где это проявится :) В общем, я думаю, что проблема несколько надуманна.
Либо при сравнении A > B будет сначала идти ненужное
if (A< B) return XXX_ordering::less;
а только потом сработающее
if (A > B) return XXX_ordering::greater;
либо наоборот.
А вот это заведомо плохой код. Вам нужно вызывать оператор A <=> B.
Суть оператора spaceship — не в оптимизации скорости выполнения, а в значительном сокращении количества кода.
Точно так же. Предполагается, что этот оператор существует для всех типов.
Часто нужно делать доп вычисления, например.
C++ уже давно стал языком, который не особо жизнеспособен без оптимизирующего компилятора. Так что никаких проблем с производительностью не будет.
Например, стандартное сравнение считает что «15» < «2». В зависимости от задачи это может быть как приемлемо, так и не приемлемо. Поэтому обсуждать абстрактное сравнение срок в отрыве от конкретной задачи достаточно бессмысленное дело.
Удобней же структуры сравнивать по другому, по очереди для всех полей проверяешь что они не равны, и если это так возвращаешь первый меньше второго, ну и с последним так же. Получается n сравнений в худшем случае, вместо 2*n-1, более того они могут быть легковеснее, так как сравнение на неравенство может быть реализовано эффективнее сравнения на меньше.
Можете пояснить мысль? Идея оператора <=> возникла не на пустом месте. Процессору нужна всего одна инструкция, чтобы вычислить все 6 операций сравнения для целых чисел.
Вместо:
bool operator< (A const& rhs) const {
if (t < rhs.t) return true;
if (rhs.t < t) return false;
return u < rhs.u;
}
Использую:
bool operator< (A const& rhs) const {
if (t != rhs.t) return t < rhs.t;
return u < rhs.u;
}
Мой вариант может быстрее если t и rhs.t лексикографические строки разной длины, тогда если для сравнения их на меньше нужно пробежать до первого расхождения, а в сравнении на неравенство — вначале стоит проверка на длину.
Чисто теоретически компилятор может все понять и соптимизировать до эквивалентного кода — но на практике это не работает: код.
Спейсшип должен решить эту проблему.
Ну не преувеличивайте.. в C++17 чтобы сравнивать без учета регистра достаточно создать кастомный std::char_traits
Операции сравнения в C++20