Комментарии 100
А если Вы делаете что-то на уровне битового представления типов, то обычно это делаете под конкретную платформу, либо реализуете это с применением приемов адаптации под другие платформы.
то этого всего Вы скорее всего не встретите
Ну некоторые моменты никак библиотекой не спрячешь. Например, в результате вызова std::abs можно получить отрицательное число на любой платформе. А уж сколько было уязвимостей из-за целочисленное переполнения и сколько еще будет.
Например, в результате вызова std::abs можно получить отрицательное число на любой платформеПограничный случай с -128/127 (и другие пределы) или что-то более интересное?
Сколько ещё в нескольких тонкостях. Скажем, Вы понимаете, что и почему напечатает: ` std::cout << std::abs(INT8_MIN) << std::abs(INT16_MIN) << std::abs(INT32_MIN) << std::abs(INT64_MIN) '?
Я, к своему стыду, абсолютно не понимаю. Можно ткнуть носом, что почитать для понимания?
C17: 7.22.6 Integer arithmetic functions
C17: 7.20.2.1 Limits of exact-width integer types
C++20: 6.8.4 Integer conversion rank
C++20: 7.3.6 Integral promotionsп
Первое значение «std::cout << std::abs(INT8_MIN)» — строго полностью определённое поведение, последнее значение «std::cout << std::abs(INT64_MIN)», на подавляющем числе систем, строго неопределённое поведение.
Вы понимаете, что и почему напечатает: ` std::cout << std::abs(INT8_MIN) << std::abs(INT16_MIN) << std::abs(INT32_MIN) << std::abs(INT64_MIN)Да, понимаю, потому что базовый тип для std::abs — int и первые два значения будут расширены до него.
Но, опять же, эксплуатация ровно одного пограничного случая.
А так, да, С++20 ссылается на C17, а в C17 для abs(), labs() и llabs(), прописано, что если результат не может быть представлен, то поведение неопределённое (с отдельным напоминанием за наименьшее целое, в случае, дополнительной арифметики). Ну, а в других функциях могут быть свои заморочки.
Или, как вариант, какая-нибудь военная/ядерная/авиационная/космическая libc выдаст иной вариант неопределённого поведения, отличный от типичного std::abs(INT64_MIN) == llabs(INT64_MIN) == INT64_MIN
Я, в принципе, не против фантастики, но предпочитаю её обсуждать в отдельных темах.
Я вполне себе могу представить компилятор, который операции с int16_t не векторизует, т.к. стандарт требует для них дополнительной арифметики, а int_fast16_t векторизует.
std:abs
, извините? Представление чисел важно не только для std:abs
, но и для банального a = b;
. Тут нет неопределённого поведения при любом целочисленном a
и любом целочисленном b
.Так же, разрешите напомнить, что в стандарте C17, на который ссылается C++20, указано «The abs, labs, and llabs functions compute the absolute value of an integer j. If the result cannot be represented, the behavior is undefined.»
Для некоторых, редких, вариантов реализаций int/long/long long, такая ситуация неопределённого поведения невозможна, так же как для некоторых, редких, компиляторов/библиотек это неопределённое поведение может быть нетипичным.
Кроме того, какие фокусы, бывает, выкидывают современные оптимизирующие компиляторы для программ с неопределённым поведением, наверное, не мне Вам рассказывать.
std::abs(INT64_MIN) == llabs(INT64_MIN) == INT64_MINэто вы ещё сравнивать вещёственный числа не пробовали видимо :)
float a = 3.5;
float b = 2.0 + 1.5;
if (a == b) { ... // <- так сравнивать нельзя
Эх, было время, я длинное целое умножение/деление на плавающем сопроцессоре делал.
Знать как нельзя, это действительно большое достижение, многие даже и не знают, но некоторым бывает интересен ответ на вопрос почему.
bool is_equal( float a, float b ) {
return ( a == b );
}
И нет, и это не частный, случай. В предыдущем тезисе Вы, по-моему, так и не выделили три различные проблемы, а свалили всё в одну кучу.
А здесь, так это вообще множество общих случаев.
Просто к ошибкам округления, как и ко всему остальному, стоит подходить с пониманием. Есть алгоритмы, когда их можно считать случайными. Есть алгоритмы в которых их следует учитывать для правильного выбора констант разложений и т.п. (0.125 — точно, 0.1 — неточно, /10 — точно). Есть алгоритмы, где они «самоуничтожаются» (скажем, Кнут т. 2, s = a + b; e = b — (s — a);). А есть алгоритмы где их вообще не возникает.
И да, в юнит-тестах, регрессионных тестах, а так же в тестах самотестирования на объекте, double сравниваются на равенство. Что б поймать за руку какого-нибудь молодого, да раннего, инженера по сборке, если он накосячит. Или сторонний модуль, если он что-то в настройках FPU изменит.
Я бы это назвал, практик, вычислитель, вычислительная математика она ж такаяМатематик-теоретик, не программист-практик, уж извините :)
И да, в юнит-тестах, регрессионных тестах, а так же в тестах самотестирования на объекте, double сравниваются на равенство
github.com/google/googletest/blob/master/docs/advanced.md#floating-point-comparison
они ссылаются сюда
randomascii.wordpress.com/2012/02/25/comparing-floating-point-numbers-2012-edition
Уверен, что вы читали эту статью или подобную, но ответ:
к ошибкам округления, как и ко всему остальному, стоит подходить с пониманием. Есть алгоритмы, когда их можно считать случайными. Есть алгоритмы в которых их следует учитывать для правильного выбора констант разложений и т.п. Есть алгоритмы, где они «самоуничтожаются». А есть алгоритмы где их вообще не возникает.как раз и написал математиком-теоретиком, а не программистом-практиком, или прагматиком, если хотите.
Грубо говоря,
for(i = 0; i < sqrt(n); i++)
a[i*i] = 1.;
Совершенно не требует «улучшайзинга». ;) Если в контексте уместно, то вместо «i < sqrt(n)» столь же корректно написать «i != sqrt(n)» ;) Если n < 252, конечно, но компьютеры с памятью больше 255, есть только у китайцев. ;) И таких примеров вагон и маленькая тележка.
(a — b) < epsion
Заметьте: «или оно дефолтное»!
Нет, ну если процессорного времени вагон и маленькая тележка? Почему нет? Но Роскосмос, НАСА, как и Илон Маск, лично, не одобрят.
char: минимум 8 бит в ширину;но ни слова про уже, практически столь же стандартные (u)int8_t/(u)int_16_t и т.д.
short: минимум 16 бит и при этом не меньше char;
int: минимум 16 бит и при этом не меньше short;
long: минимум 32 бит и при этом не меньше int;
long long: минимум 64 бит и при этом не меньше long.
И позже, внезапно:
что? откуда взялось uint8_t если до этого про этот тип ни слова?uint8_t len = (...); for (uint8_t i = 0; i < len; i++) { ... }
Пусть uint32_t = unsigned char, и int равен 33-битам.Откуда взялся теоретик ни когда не слышавший о моделях ILP32, LLP64, LP64? Экзотика, конечно встречается, но и тогда есть определённый ряд соглашений, иначе ни одну библиотеку под эту платформу портировать не удастся, а, скажем, писать реализацию TLS 1.3/1.4 самостоятельно вряд ли кто захочет.
В stdint.h определена конкретная ширина типов: uint8_t, int8_t, 16, 32 и 64. Будьте внимательны к операциям, подразумевающим продвижение типов. Например, uint8_t + uint8_t даст int (со знаком и шириной не менее 16 бит), а не uint8_t, как можно было предположить.
Вы не очень внимательно читаете
uint8_t может быть не определён, например, на процессоре с минимальным адресуемым словом в 16 бит. Но тип, скажем, uint_fast8_t всегда есть
Преобразование указателя в int и обратно в указатель происходит без потерьНа x64, ожидаемо указатели 64 битные (а int 32 битный).
Присутствие signed и unsigned версии каждого целочисленного типа удваивает количество доступных вариантов
Было дело, я сначала плевался на Java: как из-за отсутствия unsigned переносить код с с/с++? Потом восхищался C#: «ну вот там же сделали, всё для людей!»
Потом понял: те операции, для которых (тогда мне было) это важно — они вообще не про арифметику были, а про AND, OR, XOR, где вообще понятия «знаковости» нет. Но в коде, который довелось переносить — почему-то через "+" и "-" многое было сделано.
Старые 36-битные мейнфреймы имели char 9 бит
А как int
может быть шириной 33 бита, если ранее вы пишете
Битовая ширина должна быть кратна 2.
?
Но перешли же?
unsigned long long = -1;
Я ведь правильно понимаю, что это эквивалентно
unsigned long long = -1ull;
?
Тоесть если к результирующему значению прибавить 1, то получится 0?
В статье об этом сказано, но не очень понятно
unsigned long long = -1;Просто общепринятый трюк с переполнением для получения 0xFFFF…
Я ведь правильно понимаю, что это эквивалентноРезультат — да, преобразования — нет.
unsigned long long = -1ull;
Это определено стандартом. signed-to-unsigned implicit conversion.
Although the definition uses -1, the value of unsigned type is the largest positive value it can hold, due to signed-to-unsigned implicit conversion. This is a portable way to specify the largest value of any unsigned type.
Кстати, автор в 4 пункте раздела "Заблуждения" остерегает использовать этот трюк.
Ни разу ни плюсовик, но часто сталкиваюсь с тем что приходится спорить даже с "продвинутыми" плюсовиками.
Из недавнего, спор о длине(размере) char. Ни один из трёх не знал что стандарт не гарантирует длину в 8 бит. Прямо как откровение. И так с каждым положением.
Могу сделать совет начинающим вайтишникам — не начинайте учить плюсы и джаваскрипт если вам дорого ваше психическое здоровье, и вы хотите быть уверенным до конца хоть в чём то.
ПС А ведь в статье слегка освещены проблемы только с целочисленными типами. А сколько там граблей с вещественными числами — мама не горюй...
Ни один из трёх не знал что стандарт не гарантирует длину в 8 бит.Тоже мне проблема. Пишем:
[[maybe_unused]] std::int8_t we_need_8bit_char;
Всё, стандарт резко загарантировал длину 8 бит.
Стандарт действительно гарантирует 8битный
char
, на платформах, где вышеописанный код скомпилируется.И, соотвественно, любой код, в котором есть хотя бы одна переменная типа
std::int8_t
— тоже может полагаться на 8-битный char
.В чём проблема-то?
Вот скажем «классический пример»:
switch (-1&3)
{ case 1: /* ... */ break;
case 2: /* ... */ break;
case 3: /* ... */ break;
}
когда сработают ветки case 1 и case 2?
Похоже что с практической точки зрения ничего кроме двоично-дополнительного кода для представления отрицательных чисел не существует.
stackoverflow.com/questions/12276957/are-there-any-non-twos-complement-implementations-of-c
Но можно посмотреть на вопрос немного шире. Например, числа с плавающей точкой хранятся не в двоично-дополнительном коде, но тем не менее операции над ними иногда делают, приводя их к целочисленным ради быстродействия, поэтому помнить о других возможных представлениях надо.
char, signed char и unsigned char — это три разных типа.Лет 10 назад и (un)signed bool работало, и сейчас может сработать для чистого Си, где bool объявлен как какой-нибудь char или int :)
> process(array[i — 1]);
> }
for (unsigned int i = len; i-- > 0;) {
process(array[i]);
}
Самое забавное, что комитет только к C++20 выкатил ssize() и только к C++23 родил суффиксы для ptrdiff_t и size_t (и наотрез отказывается вводить ssize_t).
Скоро никаких static_cast'ов, надо только подождать...
А всё остальное можно признать морально устаревшим, и просто выбросить.
Недавно я делал 96-битную арифметику на стандартном Си. Только сложение и вычитание. Сложение получилось быстро, с вычитанием пришлось долго повозиться. Затраты времени на это оказались в десятки раз выше, чем на написание соответствующего кода на ассемблере.
Присутствие signed и unsigned версии каждого целочисленного типа удваивает количество доступных вариантов. Это создает дополнительную умственную нагрузку, которая не особо оправдывается, так как типы со знаком способны выполнять практически все те же функции, что и беззнаковые.
Если все корректно работает, то умственной нагрузки нет. Ведь правило простое: если число может быть отрицательным, то signed. Просто, не правда ли? Проблема в другом.
Главное, чтобы это правило в обратную сторону не работало. Если число не может быть отрицательным, то его НЕ нужно делать unsigned.
Правило скорее такое: используйте signed везде, кроме мест, где нужно играться с битами.
Ведь правило простое: если число может быть отрицательным, то signed. Просто, не правда ли?Все так, если бы этому правилу всегда следовали. А то ведь time_t, например, знаковый, с той целью, чтобы функции, возвращающие time_t значения, могли вернуть -1 в случае ошибки. А был бы он беззнаковым, могли бы не париться про 2038 год.
Боюсь, лет через 5 С++ будет эдаким Паскалем/Делфи, который по инерции вроде ещё изучается и даже используется в старых проектах, но новых практически нет.
Разумеется, и там можно выстрелить в ногу, если постараться, но там компилятор, хоть и тормозной, даёт куда больше гарантий, а всё или почти всё, что даёт UB, разрешено только в unsafe-блоках, и это очень небольшая часть языка и стандартной библиотеки, нужная для нетривиальных случаев.
Разумеется, и там можно выстрелить в ногу, если постараться, но там компилятор, хоть и тормозной, даёт куда больше гарантий, а всё или почти всё, что даёт UB, разрешено только в unsafe-блоках, и это очень небольшая часть языка и стандартной библиотеки, нужная для нетривиальных случаев.
Верно. Но ошибка в unsafe-блоке — это тоже ошибка. В расте ряд вещей сделан «более по уму», чем в C/C++, и так и должно быть — он не вынужден тянуть на себе бремя совместимости длиной в полсотни лет, но он от этого не становится какой-то серебряной пулей. По-прежнему нужно держать многое в голове, особенно в случае «нетривиальных случаев», да.
неявные преобразования— это конечно самая большая неприятность
Если бы меня спросили, что в первую очередь улучшить в С/C++, то первые в расстрельном списке это неявные преобразования между различными целыми, а также между целыми и числами с плавающей точкой…
Тяжкое бремя совместимости висит над ним. Ну так хотя бы добавили прагму для отключения…
Но это скорее сделали так, потому что у крупных ядер AVR есть инструкции, типа movw и регистровые пары, используемые в качестве указателей.И всё это приводит к тому, что понятие «разрядность вычислительной архитектуры» становится понятием не шибко определённым.
Мне даже никто так и не смог объяснить, почему Z80 — 8-битный, 8086 — 16-битный, а Pentium4 — 32-битный. Потому что по размерам регистров у вас Z80 получается 16-битным (там IX и IY есть и операции с ними), а по размеру ALU у вас Pentium4 окажется 16-битным (там ALU работает с половинками регистров на удвоенной частоте).
почему Z80 — 8-битный, 8086 — 16-битный, а Pentium4 — 32-битный. Потому что по размерам регистров у вас Z80 получается 16-битнымШина адресов да, 16 бит, но данные всё равно читались по 8 бит. Такая же история и с i8088/КР580ВМ, где шина адреса тоде была 16 битной, а данные всё равно читались по 8 бит.
По этой причине процессоры и были 8-битными, первый полноценно 32-битный (из известных) считается i386, где все регистры были сделаны 32-битными, у его предшественника — i286, регисты шины адреса были 24-битными, а данных — 16, при этом было несколько регистров по 48 бит, 40 и даже 64.
Шина адресов да, 16 бит, но данные всё равно читались по 8 бит. Такая же история и с i8088/КР580ВМ, где шина адреса тоде была 16 битной, а данные всё равно читались по 8 бит.Вот только Z80 — считается вершиной 8-битных процессоров, а i8088/КР580ВМ — считаются 16-битными.
По этой причине процессоры и были 8-битными, первый полноценно 32-битный (из известных) считается i386Что значит «полноценно 32-битный», извините? А 68k куда дели?
Некоторые, правда, говорят, что первенец, 68000 — типа 32/16 битный, так как ALU у него 16 битное. Но вот Prescott 64/30-битным не называют. Впрочем 68020 всё равно на год раньше 80386го появился.
при этом было несколько регистров по 48 бит, 40 и даже 64.Современные процессоры имеют регистры аж до 512 бит. Но 512-битными их никто не называет.
В общем битность процессора это дело такое, зависящее от документации, а не от реальной архитектуры. Потому с ней размер
int
и не совпадает.i8088/КР580ВМ — считаются 16-битными.Эмммм… нет
КР580ВМ80А — 8-разрядный микропроцессор.
ru.wikipedia.org/wiki/%D0%9A%D0%A0580%D0%92%D0%9C80%D0%90
С i8088 это я ошибся, КР580 был клоном i8080
Что значит «полноценно 32-битный», извините? А 68k куда дели?А вы ремарку прочитали или сразу спорить?
Ни куда не дел, так как оригинальный 68к имел 16-битные разряды данных, о чём прямо даже в вики написано
RU: из-за чего иногда процессор описывается как имеющий смешанную битность 16/32Полноценный 32-битный 68020 вышел конечно раньше i386, на год, примерно, на нём даже был выполнен Apple II, если не ошибаюсь.
EN: Generation one (internally 16/32-bit...
В общем битность процессора это дело такое, зависящее от документации, а не от реальной архитектурыОб этом и речь, но ведь оригинальный вопрос был, почему Z80 не 16-битный, потому что набор команд и шина данных 8-битные, а размер регистров не определяет битность процессора.
С i8088 это я ошибся, КР580 был клоном i8080А я только на i8088 и глянул, не обратил внимания, что вы не про КР580ВМ88.
Об этом и речь, но ведь оригинальный вопрос был, почему Z80 не 16-битный, потому что набор команд и шина данных 8-битные, а размер регистров не определяет битность процессора.Вот только в наоборе команд есть и 8-битные и 16-битные регистры, а шина данных такая же 8-битная, как и у 8088.
И у того же «смешанного» 68000 регистры и система команд таки 32-битная.
а размер регистров не определяет битность процессора.А что её определяет? Вот почему у вас 8088й резко стал 16-битным, аж даже извиняться и «отыгрывать» пришлось?
И почему послений, доживший до сегодня, потомок Z80, имея 24-битные регистры и ALU считается, тем не менее, 8-битным — из-за архитекуры, типа, а 68000, имеющий 32-битную архитектуру — не считается 32-битным? Хотя совместимый с ним 68020 (гораздо более совместимый, чем 80386: там старые программы работают в особом, 16-битном, режиме) — полноценно 32-битный?
вы не про КР580ВМ88я даже специально загуглил и могу ответить только цитатой с одного из форумов
Очень интересно, учитывая, что КР580ВМ88 не существует, а КР580ВМ80 — это i8080, с i8086/88 не совместимый.
Вот почему у вас 8088й резко стал 16-битным, аж даже извиняться и «отыгрывать» пришлось?
потому как i8088 это переделка i8086, 16-битного процессора под 8-битные микросхемы памяти, а извинялся я за опечатку, вместо 8080 я напечатал 8088.
а шина данных такая же 8-битная, как и у 8088Тут да, но оперировал 8088 16-битными данными, а даунгрейд был сделан по техническим причинам
The main difference is that there are only eight data lines instead of the 8086's 16 lines. All of the other pins of the device perform the same function as they do with the 8086 with two exceptions. First, pin 34 is no longer BHE (this is the high-order byte select on the 8086—the 8088 does not have a high-order byte on its eight-bit data bus).
Ну то есть 8088 читал побайтно даже 16-битные данные (и писал так же), так как 8-битных чипов память наделали столько, что нужно было куда-то их реализовывать.
а 68000, имеющий 32-битную архитектуру — не считается 32-битным? Хотя совместимый с ним 68020 (гораздо более совместимый, чем 80386: там старые программы работают в особом, 16-битном, режиме) — полноценно 32-битный?Если хотите спорить ради спора, то хотя бы читайте первоисточники, хоть какие-нибудь.
The Motorola 68000…
…
The design implements a 32-bit instruction set, with 32-bit registers and a 16-bit internal data bus. The address bus is 24-bits and does not use memory segmentation, which made it popular with programmers. Internally, it uses a 16-bit data arithmetic logic unit (ALU) and two more 16-bit ALUs used mostly for addresses, and has a 16-bit external data bus. For this reason, Motorola termed it a 16/32-bit processor.
The 68020 added many improvements over the 68010 including a 32-bit arithmetic logic unit (ALU), 32-bit external data and address buses, extra instructions and additional addressing modes.
(гораздо более совместимый, чем 80386: там старые программы работают в особом, 16-битном, режиме)Ну то есть появление в i386 принципиально иных режимов, как то, страничное управление памятью, защищённый режим работы процессора, который и заставил делать целый слой совместимости со старыми процессорами — виртуальный режим, это фигня и её можно сравнивать с эволюцией линейки 68k?
Ну то есть, ещё раз, 68020 и i386 в реальном режиме как бы да, сравнимо, но i386 в защищённом режиме это как бы совершенно отдельная история.
Для 68020 и 68000 нет никакого 'реального режима'. Они всегда работают с полноценными 32-битными регистрами и полноценной 32-битной адресацией (из которой только 24 бита адреса вылазят наружу для 68000, но уже все 32 бита для 68020). В противовес 8086 с ужасной 16-битной адресацией через сегменты, навёрнутом на основе этого в 80286 защищённом режиме и наконец расширении всего этого унылого безобразия до 32 бит с добавкой виртуальной страничной памяти в 80386.
Взамен всей этой кучи бессмысленной ерунды в 68020 просто добавили страничную MMU (внешним чипом, с 68030 MMU уже на кристалле процессора).
Свод правил по работе с целыми числами в C/C++