Как стать автором
Обновить

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

одной из наиболее подверженных ошибкам частей C++ являются выражения с целочисленными типами и типами с плавающей запятой.

Я бы добавил туда "у новичков". В оригинале это упоминание тоже отсутствует. Может быть, автор пишет серию для начинающих. Целая статья, вместо того чтобы просто объяснить их частую ошибку в выражении float f = a / b , где a и b - целые.

Интересно что мешает в C++ добавить целый тип произвольной длинны, как в питоне?
print(2**300)

2037035976334486086268445688409378161051468393665936250636140449354381299763336706183397376

Ничего не мешает (есть библиотеки), важно только помнить, что оно не поддерживается процессором на аппаратном уровне, и работает медленно.

В процессорах есть сложение и вычитание с переносом. Что бы это использовать эти команды в C++ придётся использовать костыли или писать на ассемблере. Что мешает это встроить в компилятор как штатный тип данных?

вы очевидно не понимаете чего просите. Вы знаете что придется выделять память на число в этом случае?

Можно подумать что строки память не выделяют. И потом что мешает помимо динамических иметь фиксированные размеры этой памяти (есть же шаблоны с параметрами).
  1. Не во всех процессорах сложение и вычитание с переносом есть.

  2. Как правильно указали - один из ключевых вопросов: как и где выделять память и как оно будет лежать в ней. В том числе - вопросы констант времени компиляции. И обосновать, почему именно такой layout должен поддерживаться компилятором. И это надо будет поддерживать всем компиляторам для всех платформ.

  3. Кроме сложения и умножения есть еще много операций. С ними свои проблемы. Для умножения/деления надо делать карацубы всякие. Для битовых сдвигов и and/or/xor либо хранить в двоичном дополнении, либо при выполнении операций туда-сюда преобразовывать. Для операций преобразования - прорабатывать дизайн.

  4. Даже для "произвольной" длины надо обговорить границы (например, 2^31-1 бит - а это не так много в современном мире) и корректно обработать в коде.

  5. Для литералов надо аккуратно встроить их в дизайн языка. Для типа в целом - решить вопросы обратной совместимости кода (шаблонов и т. п.)

Короче - гора работы, которая на уровне языка, декларирующего прозрачность абстракций, никакого выигрыша по сравнению с отдельной библиотекой скорее всего не даст. А библиотеки уже и так есть.
Python и 1С поддерживают такие числа "из коробки" - но это именно потому что они явно могут себе позволить быть небыстрыми (и тот же Python не поддерживает такие числа в производительных библиотеках). JS (EcmaScript) лишь относительно недавно добавил такие числа (и это явно не его киллер-фича). JVM, dotnet - поддерживают их как отдельные библиотеки в рамках стандартной библиотеки. Rust - отдельные библиотеки, С++ - отдельные библиотеки (и весьма крутые). При этом реально числа неограниченной длины нужны очень нечасто и ограниченно.

Короче, работы много, а запроса неудовлетворённого фактически нет.

1,2,3,4,5… можно подумать с тем зоопарком что есть эти операции не реализованы: 1U,1UL,1L,0xFULL

Так язык планируют улучшать или как. Если штатные типы вводят в ступор новичков, им можно работать с типом который отлавливает переполнения или не переполняется в разумных пределах доступных ресурсов. Когда освоится сможет и задумается о ресурсах будет применять std::int_fast32_t или даже по отдельным битам складывать.
Не во всех процессорах сложение и вычитание с переносом есть

Приведите хотя бы 2 примера таких процессоров.

Почти любой производительный RISC процессор. Там тебе придётся считать числа как signed и ловить, довольно дорогие, исключения от процессора, просто ради того чтобы посчитать расширенное число. Фичи с флагами как в х86 есть далеко не у каждой платформы.

(Давно в другие платформы не тыкал, но допустим тот де MIPS очень долго не имел флагов переноса. RICSV делают те же люди что и мипс, потому не думаю что там он будет)

Переводя дед выше говорит, что идеология языка в том, чтобы создать инструмент для сборки чего угодно и для максимально точного задания поведения. Ты в языке можешь даже перегружать методы класса для lvalue/rvalue ссылок, лол. Делать инструменты для типовой работы это задача не комитета языка а сообщества.

RISC-V делают водолазы им можно.

Тем не менее даже в таких убогих архитектурах существуют приемлемые варианты решения данной задачи. И есть костыли для их использования.

Отсутствие флагов это не убогость, а преимущество. Все современные архитектуры отказываются от флагов, т.к. это лишнее глобальное состояние, из-за которого в конвейере процессора даже не зависимые по данным команды начинают зависеть друг от друга, а это очень сильно мешает распараллеливанию выполнения.

Яб поспорил, всё же грамотные реализации с флагами будут лучше чем без них, другая проблема что это отдельная сущность которую надо делать и которая жрёт транзисторы. Плюс компиляторы давно уже не имеют нормальной поддержки тех хитровыдуманных инструкций которые есть в 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

Выглядит очень не плохо, на мой взгляд.

Конечно же 6 команд выглядит лучше чем одна.

Современные процессоры устроены совсем не так, как в 80-х. Здесь, мне кажется, нужны замеры - насколько это медленнее. Нельзя же производительность в этом случае линейно, по количеству команд, мерить.

Как бы они не были устроены тут дополнительные вычисления используются. Вместо 1 сложения будет 3 сложения 2 вычитания и один or. Как бы они ни были устроены 1 операция против 6 операций потребляют немного разное количество ресурсов.

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

Мой любимый пример:

#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 разве не логический тип?

Не-а :) Однобитный целочисленный в контексте статьи. На английском используются два термина: integer type и integral type. Так вот bool - integral, но не integer.

Зарегистрируйтесь на Хабре, чтобы оставить комментарий