Comments 31
одной из наиболее подверженных ошибкам частей C++ являются выражения с целочисленными типами и типами с плавающей запятой.
Я бы добавил туда "у новичков". В оригинале это упоминание тоже отсутствует. Может быть, автор пишет серию для начинающих. Целая статья, вместо того чтобы просто объяснить их частую ошибку в выражении float f = a / b
, где a
и b
- целые.
Я думаю, что автор имеет в виду в том числе и UB - например, https://github.com/Nekrolm/ubbook/blob/master/numeric/overflow.md и https://github.com/Nekrolm/ubbook/blob/master/numeric/integer_promotion.md
print(2**300)
2037035976334486086268445688409378161051468393665936250636140449354381299763336706183397376
Ничего не мешает (есть библиотеки), важно только помнить, что оно не поддерживается процессором на аппаратном уровне, и работает медленно.
вы очевидно не понимаете чего просите. Вы знаете что придется выделять память на число в этом случае?
Не во всех процессорах сложение и вычитание с переносом есть.
Как правильно указали - один из ключевых вопросов: как и где выделять память и как оно будет лежать в ней. В том числе - вопросы констант времени компиляции. И обосновать, почему именно такой layout должен поддерживаться компилятором. И это надо будет поддерживать всем компиляторам для всех платформ.
Кроме сложения и умножения есть еще много операций. С ними свои проблемы. Для умножения/деления надо делать карацубы всякие. Для битовых сдвигов и and/or/xor либо хранить в двоичном дополнении, либо при выполнении операций туда-сюда преобразовывать. Для операций преобразования - прорабатывать дизайн.
Даже для "произвольной" длины надо обговорить границы (например, 2^31-1 бит - а это не так много в современном мире) и корректно обработать в коде.
Для литералов надо аккуратно встроить их в дизайн языка. Для типа в целом - решить вопросы обратной совместимости кода (шаблонов и т. п.)
Короче - гора работы, которая на уровне языка, декларирующего прозрачность абстракций, никакого выигрыша по сравнению с отдельной библиотекой скорее всего не даст. А библиотеки уже и так есть.
Python и 1С поддерживают такие числа "из коробки" - но это именно потому что они явно могут себе позволить быть небыстрыми (и тот же Python не поддерживает такие числа в производительных библиотеках). JS (EcmaScript) лишь относительно недавно добавил такие числа (и это явно не его киллер-фича). JVM, dotnet - поддерживают их как отдельные библиотеки в рамках стандартной библиотеки. Rust - отдельные библиотеки, С++ - отдельные библиотеки (и весьма крутые). При этом реально числа неограниченной длины нужны очень нечасто и ограниченно.
Короче, работы много, а запроса неудовлетворённого фактически нет.
Так язык планируют улучшать или как. Если штатные типы вводят в ступор новичков, им можно работать с типом который отлавливает переполнения или не переполняется в разумных пределах доступных ресурсов. Когда освоится сможет и задумается о ресурсах будет применять std::int_fast32_t или даже по отдельным битам складывать.
Не во всех процессорах сложение и вычитание с переносом есть
Приведите хотя бы 2 примера таких процессоров.
Почти любой производительный RISC процессор. Там тебе придётся считать числа как signed и ловить, довольно дорогие, исключения от процессора, просто ради того чтобы посчитать расширенное число. Фичи с флагами как в х86 есть далеко не у каждой платформы.
(Давно в другие платформы не тыкал, но допустим тот де MIPS очень долго не имел флагов переноса. RICSV делают те же люди что и мипс, потому не думаю что там он будет)
Переводя дед выше говорит, что идеология языка в том, чтобы создать инструмент для сборки чего угодно и для максимально точного задания поведения. Ты в языке можешь даже перегружать методы класса для lvalue/rvalue ссылок, лол. Делать инструменты для типовой работы это задача не комитета языка а сообщества.
Тем не менее даже в таких убогих архитектурах существуют приемлемые варианты решения данной задачи. И есть костыли для их использования.
Отсутствие флагов это не убогость, а преимущество. Все современные архитектуры отказываются от флагов, т.к. это лишнее глобальное состояние, из-за которого в конвейере процессора даже не зависимые по данным команды начинают зависеть друг от друга, а это очень сильно мешает распараллеливанию выполнения.
Яб поспорил, всё же грамотные реализации с флагами будут лучше чем без них, другая проблема что это отдельная сущность которую надо делать и которая жрёт транзисторы. Плюс компиляторы давно уже не имеют нормальной поддержки тех хитровыдуманных инструкций которые есть в x86, а поддерживать приходится. (А на асме ща пишут разве что SIMD, как по мне)
BigInt это всё ещё ненужная фича. Ибо она буквально по разному работает на разных платформах.
Комитет такие вещи очень сильно не любит и со скрипом занимается подобными вещами. И примеров когда такие вещи всё же вводят а в итоге они работают через жопу, хватает.
Потому-что на практике это не сложно сделать в виде сторонней библиотеки, а это принцип С++ - не тащить в компилятор то, что не нужно.
Есть подход big integer - когда число не ограничено по длине и фактически его придется хранить в динамической памяти (привет Питону). В этом случае придется расплачиваться быстродействием.
Есть подход long integer - когда необходимо работать с числами большими, чем поддерживаются процессором, но при этом фиксированной длины. Такие числа можно хранить на стеке и работать с ними почти также быстро как со встроенными, с поправкой на длину. Например, данный подход используется в Simple long integer math library for C++. Такие числа почти взаимозаменяемые со стандартными, не считая некоторых нюансов в автоматических преобразованиях.
Он и так это без костылей оптимизирует: adc, sbb всякие, simd где может использует. О каких костылях речь ? На 32-х битной архитектуре 64-х битные целочисленные операции (long int) уже через вызовы отдельных функции выполняются, а как иначе, это костыли?
Это будет в два раза медленнее, чем это возможно. Зачем так делать? Я с трудом представляю архитектуру, где не было бы adc, но даже, если это так, то перенос легко вычислить для полной суммы:
value1 += value2;
bool carry = value1 < value2;
Задача такая:
// adc value1,value2
value1 = value1 + value2 + carry;
carry = ...;
В любом случае, если в архитектуре нет команды adc, то будут накладные расходы, но не большие.
constexpr bool addc(uint32_t& value1, uint32_t value2, bool carry) noexcept
{
value1 += value2;
bool carry_new = value1 < value2;
value1 += carry;
carry_new = carry_new || (value1 < uint32_t(carry));
return carry_new;
}
Вот например выхлоп для RISC-V:
add a0, a0, a1
sltu a1, a0, a1
add a2, a2, a0
sltu a0, a2, a0
or a0, a0, a1
add a0, a0, a2
Выглядит очень не плохо, на мой взгляд.
Современные процессоры устроены совсем не так, как в 80-х. Здесь, мне кажется, нужны замеры - насколько это медленнее. Нельзя же производительность в этом случае линейно, по количеству команд, мерить.
Продвижения, как правило, вполне безобидны и практически незаметны, но могут сваливаться как снег на голову...
Мой любимый пример:
#define BIT_MASK(bit) (~(1 << (bit)))
В результате сотрясение мозга: https://coliru.stacked-crooked.com/a/ecdcb441287496c4
интересно, а объяснение есть, почему так?
Результат макроса имеет тип int. А дальше
Когда беззнаковый операнд имеет тот же или более высокий ранг, чем знаковый операнд, знаковый операнд преобразуется в тип беззнакового операнда.
Он преобразуется в uint64_t. При этом результат макроса положительный, знаковый бит у него сброшен. Поэтому старшие биты оказываются нулевыми.
В первом же выражении знаковый бит установлен, и при расширении до 64 бит старшие биты дополняются единицами.
Вообще, обратная совмесимость - зло, из-за нее множество проблем, в том числе и с числами.
Прежде всего, я бы отметил то, что в C/С++ (как и во многих языках) числовые литералы по умолчанию типизированы, что ИМХО неверно. Для типизации есть всякие постфиксы типа "u", "s" или "l", а вот просто числовые литералы (равно как и просто строковые литералы) должны быть нетипизированными, абстрактными, и свободно приводиться алгоритмами вывода типа к любым типам. Чем должен быть например литерал 0? Это может быть и байт, и целое, и число с плавающей точкой, любой длины и любой точности - в зависимости от контекста; такой подход ближе всего к математике.
Отсутствует поддержка длинной арифметики "из коробки", и соответственно, нет числовых литералов любой длины (всякие GMP для представления длинных числовых литералов используют строки).
Отсутствует поддержка fixed point, что весьма странно для низкоуровневых языков, применяемых в том числе во множестве микроконтроллеров. Хотя вся поддержка реализуется в чистом виде на уровне компилятора, внутри это обычные целые числа.
bool разве не логический тип?
Daily bit(e) of C++ | С числами не так все просто