На счёт переголовы. Я кажись не совсем прав. Если реализовывать <=> внутри как-то так:
struct S { f32 x; op<=>(S a, S b) : i32 { if( a.x < b.x ) { return -1; } if( a.x > b.x ) { return +1; } return 0; } }
То при вызове оператора <=> для <, <=, >, >= переголовы не выходит. При встраивании функции компилятор лишнюю переголову выкинет.
Ещё я подумываю, что можно реализовать оператор <=> для встроенных типов для удобства реализации своего оператора <=>.
Panzerschrek[CN]
Значит проблема решена, можно тупо сделать < > внутри, вернув некий энам?
Panzerschrek[CN]
> Но мне не нравится, что он возвращает некий enum с тремя, а возможно четыремя
> значениями.
Совершенно точно вариантов должно быть как минимум четыре, иначе у тебя IEEE 754 отвалится.
Есть ещё вариант из PowerPC, общее сравнение - битовое поле с 4 флагами (less, equal, greater, unordered), частные случаи - это битовые маски:
bitfield Ordering { LT, EQ, GT, UO } trait PartialOrd { fn compare(a: &Self, b: &Self) -> Ordering; } operator < </T: PartialOrd/> ( a: &Self, b: &Self) { return compare( a, b) & LT; } operator <= </T: PartialOrd/> ( a: &Self, b: &Self) { return compare( a, b) & ( LT | EQ); } operator == </T: PartialOrd/> ( a: &Self, b: &Self) { return compare( a, b) & EQ; } operator != </T: PartialOrd/> ( a: &Self, b: &Self) { return not ( compare( a, b) & EQ); } impl PartialOrd for f32 { fn compare( a: &f32, b: &f32) { return PartialOrd{ LT = builtin_lt_f32( a, b), EQ = builtin_eq_f32( a, b), GT = builtin_gt_f32( a, b), // UO = builtin_uo_f32(a, b), // убогий апи??? // но шланг вроде понимает: https://godbolt.org/z/q6beofba5 UO = not ( builtin_lteq_f32( a, b) or builtin_gt_f32( a, b)), }; } }
1 frag / 2 deaths
> вернув некий энам?
В ядро языка какой-то enum тащить даже не нужно. Хватит какого-нибудь встроенного знакового типа.
Имбирная Ведьмочка
> у тебя IEEE 754 отвалится
Плевать на всякие NaN. Отсортируются они как-нибудь компаратором вида
if( a > b ) return -1; if( b < a ) return +1; return 0;
> битовое поле с 4 флагами (less, equal, greater, unordered)
Избыточно, ибо less и equal исключают друг друга.
Panzerschrek[CN]
> Избыточно, ибо less и equal исключают друг друга.
Видимо имеется в виду 4 возможных значения
Panzerschrek[CN]
> Плевать на всякие NaN.
Опять насаждаешь свои личные предубеждения? Таким путём у тебя получится язык, у которого единственная сфера применения - это компиляция самого себя. Потому что всё остальное ты сам же и запретил. Скриптинг? Нинужон. Веб-сервер? Нинужон. Геймдев? Нинужон. Физдижки, рейтрейсеры и софтрендеры? Тоже не пойдут - ведь это в первую очередь численные приложения, а на поддержку чисел ты прямо тут и поклал.
Panzerschrek[CN]
> Отсортируются они как-нибудь компаратором вида
>
> if( a > b ) return -1;
> if( b < a ) return +1;
> return 0;
Конкретно этот компаратор выдаст, что NaN равны всему на свете:
compare_panzerschrek(1, NaN) ==> 0 compare_panzerschrek(NaN, 2) ==> 0 compare_panzerschrek(1, 2) ==> а вот и не ноль, фиг там
Чтобы ты понимал, какая это лютая хрень, вот тебе зарисовка.
Квиксорт. Массив флоатов. Выбираем пивот. Там оказался NaN. Раскидываем по диапазонам - все элементы равны пивоту. Таким образом, обнаруживаем, что список уже отсортирован - там все элементы одного порядка. Поэтому даже не заморачиваемся с рекурсией, а выходим сразу.
Отсортированный массив: [NaN, 1, 10, -500, 0, -7e15, 8.9e26].
Panzerschrek[CN]
> Избыточно, ибо less и equal исключают друг друга.
Ну сделай енум на 4 варианта и сравнивай на равенство. Я не знаю, какой вариант LLVM оптимизирует лучше.
Имбирная Ведьмочка
> Отсортированный массив: [NaN, 1, 10, -500, 0, -7e15, 8.9e26].
Мусор на входе - мусор на выходе. Если не нравится - напиши свой компаратор, учитывающий особенности NaN.
> NaN равны всему на свете
NaN в этом смысле диалектичен - он равен и одновременно неравен самому себе.
> Веб-сервер
> Геймдев
Разрешаю.
> Физдижки, рейтрейсеры
Кстати. У крестокомпилятора есть опция, которая превращает NaN в неопределённое поведение. Думаю, во всяких физдвижках всё именно так, ибо учитывать NaN - ненужная переголова.
> софтрендеры
Писал их ни раз. Там значительная часть вычислений - на целых числах. Плавучку использовал разве что в трансформациях до непосредственно растеризации.
Имбирная Ведьмочка
> Квиксорт. Массив флоатов. Выбираем пивот. Там оказался NaN. Раскидываем по
> диапазонам - все элементы равны пивоту.
Кривая какая-то у тебя сортировка. В Ü она такая. Там идёт раскидывание по двум диапазонам по критерию компаратора. Потом диапазоны рекурсивно досортируются. В случае, если выбранный медианный элемент - NaN, все элементы кроме него самого (включая другие NaN) окажутся во втором диапазоне. В случае, если медианный элемент не NaN - опять же NaN попадут во второй диапазон. В итоге так или иначе все NaN окажутся в конце.
Теперь что же будет, если будет массив не float, а массив структур с float внутри и перегруженным оператором <=>, как я показал выше? Оператор для сравнения чего-то с NaN вернёт 0. Это означает всегда false для упорядочивания через <. Ровно так, как уже есть сейчас.
Вообще, если чисто логически подумать, то в быстрой сортировке внутри идёт рекурсивное разделение чисел по двум спискам по некоторому предикату относительно некого числа. Если это число - NaN, то предикат всегда даёт один и тот же результат для всех чисел, что значит, что в итоге будут два списка, один с NaN, другой - с остальными числами. Если же опорное число не NaN, а сравнивают с ним NaN, то результат тоже не важен, NaN попадёт в какой-то итоговый список, не важно какой.
В итоге конечно в результирующем массиве может быть раскидано куча NaN в разных позициях, но если их убрать, то массив будет корректно отсортирован.
Panzerschrek[CN]
> Если не нравится - напиши свой компаратор, учитывающий особенности NaN.
Так проще же просто другой язык взять.
Ещё задумываюсь над автогенерацией оператора ==. По идее, это поможет убрать бойлерплейт с ручным его написанием.
Для структур можно его по умолчанию генерировать. Если поля - это целые числа, массивы, кортежи, то понятно, как их сравнивать. Если есть поля - сырые указатели, то тоже более-менее понятно. Поля типов классов с имеющимся оператором == тоже понятно, как сравнивать. А вот со ссылочными полями всё сложно, не понятно, что сравнивать - значение ссылки или значение по ссылке. Так что, думаю, в таком случае пусть программист сам определяет, как ему сравнивать их.
Думаю, также, что можно будет сравнивать автоматически кортежи и массивы, если для их элементов есть оператор ==.
Обнаружил такой интересный случай:
struct S { i32 a; i32 b; op==(S& l, S& r) : bool { return l.a == r.a && l.b == r.b; } }
_ZN1SeqERKS_S1_: movl (%rdi), %eax cmpl (%rsi), %eax jne .LBB0_1 movl 4(%rdi), %eax cmpl 4(%rsi), %eax sete %al retq .LBB0_1: xorl %eax, %eax retq
Компилятор чего-то не осиливает убрать ветвление в операторе && в таком тривиальном случае. Глянул на выхлоп clang - там такой фигни нету:
_Z7COmpareRK1SS1_: movl (%rdi), %eax movl 4(%rdi), %ecx xorl (%rsi), %eax xorl 4(%rsi), %ecx orl %eax, %ecx sete %al retq
Начал смотреть в LL код, понял, в чём проблема. Clang выставляет атрибут dereferenceable(8) на ссылочных параметрах, а Ü компилятор - нет. Без этого оптимизатор считает, что нельзя заранее прочитать из памяти, а то вдруг будет выход за границы.
Мне только что в голову пришла гениальнейшая идея.
У диграфов </ и /> есть такое нежелательное свойство - они требуют переключения шифт-кнопки во время набора. В каком-то смысле, они требуют по два с половиной нажатия - после набора <, нужно сначала отжать шифт, и только после этого - нажать /. При быстром наборе, очень легко вместо этих диграфов получить <? и ?>.
Соответственно, рекомендация - вместо прямого слеша подобрать такой символ, который тоже набирается при зажатом шифте.
(Идея пришла при очередном пользовании .~ и ^.)
Имбирная Ведьмочка
> они требуют по два с половиной нажатия
Не страшно, привычка легко набирать </ /> быстро вырабатывается.
> <? и ?>
Занято
Запилил генерацию оператора == для структур и классов.
Для структур он генерируется автоматически, если все поля сравнимы на равенство и нету ссылочных полей. Для классов аналогично, но генерацию оператора надо запрашивать через = default (по умолчанию оператор не генерируется).
В добавок к этому реализовал оператор == для контейнеров vector, optional, variant, если их элементы сравнимы на равенство.
Код Компилятора1 от таких изменений весьма сократился. Ушло множество написанных вручную операторов ==. В этом и есть смысл генерации - делать компилятором тривиальные вещи за программиста.
Подумываю тут, что делать, если пользователь объявляет специальные методы (конструктор по умолчанию, конструктор копирования, оператор копирующего присваивания, деструктор, оператор сравнения на равенство) как unsafe.
Загвоздка тут в том, что эти методы компилятор вызывает самостоятельно в ряде случаев, как напрямую, так и при генерации методов других классов. Так вот, не совсем ясно, что в этом случае делать. Запрещать unsafe для этих методов? Не запрещать, но при попытке вызова проверять, что он происходит в unsafe блоке? Делать сгенерированные методы класса unsafe, если они содержат вызовы соответствующих unsafe методов для полей?
Короче, я пока не решил, как лучше поступить. Самым простым было бы тупо запретить указывать unsafe этим методам. Но возникает вопрос - вдруг таки для каких-то целей unsafe может быть полезен?