Pull to refresh

Comments 25

Чтобы понять, что здесь происходит, достаточно взглянуть на раздел 8.3.1.8 стандарта C++ 17

Чтобы понять что происходит, достаточно знать о "two’s complement". Знаете как изменить знак у знакового целого ? Нет, не инверсией старшего бита. А инверсией всех бит и добавлением 1цы.

Но это реакция на переполнение знакового целого числа.

Нет там никакого переполнения. Просто одни и те же биты интерпретируются по-разному.

Очень рекомендую почитать Hacker Delight, чтобы не делать подобных "открытий".

Нет там никакого переполнения. Просто одни и те же биты интерпретируются по-разному.

@screwer Уточните п-та, какой параграф стандарта С++ гарантирует интерпретацию битов по разному в случае знаковых типов? Ссылку на драфт я привел в конце статьи.

Очень рекомендую почитать Hacker Delight, чтобы не делать подобных "открытий".

Спасибо, обязательно почитаю!

Уточните п-та, какой параграф стандарта С++ гарантирует интерпретацию битов по разному в случае знаковых типов?

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

Нет ни одной [массовой] современной архитектуры, где целые числа работают по-другому.. И Risc/Cisc, и Be/Le, и vliw - везде дополнительный двоичный код. (Про массовость уточнил только чтобы отсеять совсем маргинальные и местечковые штучные "изобретения", кои и если вдруг найдутся).

Чтобы понять что происходит, достаточно знать о "two’s complement".

Очень рекомендую почитать Hacker Delight

Давно пора понять, что код пишется на языке программирования. Именно на нём. А не под операционную систему. И не под процессор. Потому сначала необходимо читать документацию на язык программирования. И при программировании на C++ мы (внезапно!) пишем для абстрактной машины C++. А не для вашего мирка.

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

А меня всегда удивляли люди, которые думают, что мир должен быть устроен исключительно в соответствии с их "здравым смыслом", а в неопределённом поведении, которое такой человек наговнокодил, виноваты разработчики компилятора.

На самом деле, реальная реализация всего этого безобразия на реальном железе совершенно неважна, потому что переполнение знакового целого - undefined behavior, отсутствие важного для правильной работы программы ограничения.

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

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

Уверен, при написании кода, имеет смысл полагаться на стандарт и на то, что он разрешает, запрещает как-то регламентирует.

Я не мало писал на C++ под мобильные девайсы. Мне иногда доводилось сталкиваться с ошибками, к которым приводил unspecified behaviour. Как раз та самая ситуация, когда обычно поведение одно и тоже (хотя и unspecified), но на каком-нибудь девайсе, в редком случае и только в релизной сборке оно другое. Притом, как правило, этот класс ошибок очень не просто выявить и исправить, т.к. они редко воспроизводятся.

Более того, начиная с С++20 битовое представление signed integers фиксировано стандартом как дополнение до двух (two's complement).
However, all C++ compilers use two's complement representation, and as of C++20, it is the only representation allowed by the standard, with the guaranteed range from -2^(N-1) to +2^(N-1) — 1 (e.g. -128 to 127 for a signed 8-bit type).

8-bit ones' complement and sign-and-magnitude representations for char have been disallowed since C++11 (via CWG 1759), because a UTF-8 code unit of value 0x80 used in a UTF-8 string literal must be storable in a char element object.

Уточните п-та, какой параграф стандарта С++ гарантирует интерпретацию битов по разному в случае знаковых типов?

Вот тут есть выдержка, какие бывают модели представления знаковых чисел:

https://habr.com/en/post/683714/

Прямая ссылка на документ:

https://github.com/burlachenkok/CPP_from_1998_to_2020/blob/main/Cpp-Technical-Note.md#integer-arithmetic-and-enumerations

Вроде бы из живых архитектур остались только те, что представляют знаковые целые как "two's complement code".
И это даже было внесено в стандарт С++ (не пишу на С++ - поэтому не знаю в С++17 или С++ 20).

И да опять же это вроде implementation behavior - т.е. не UB и компилятор должен создать код с некоторой семантикой (разумной) и в дальнейшем этой семантики придерживаться (в отличии от UB когда он не обязан придерживаться никакой семантики).

Ну строго говоря, существуют (антикварные преимущественно) вычислительные машины, где знаковые числа представлены не в дополнительном коде, а например, в обратном коде. И результат там будет действительно чёрти какой.

Для стандарта языка C есть предложение исключить из стандарта возможность представления знаковых чисел в обратном коде (https://www.open-std.org/jtc1/sc22/wg14/www/docs/n2218.htm), но пока воз и ныне там, ибо существуют всё ещё современные компьютеры где это актуально.

PS: вдогонку,  SEI CERT C Coding Standard имеет соответствующее предупреждение.

Вы в примере в выводах "signed_width" использовать забыли.
Да и проще написать int32_t signed_offset = -(int32_t)width;

Спасибо огромное! Поправил.

Да и проще написать int32_t signed_offset = -(int32_t)width;

Ага. Как вариант.

В плюсах в таких случаях принято использовать static_cast

Старые привычки трудно изживаются :-(. Да и раздражают телеги текста там, где хотел просто константу написать (а вот в более сложных случаях новые привычки могут выручить, да).

В плюсах принято использовать std::make_signed, ибо неизвестно кастить к какому именно (с какой разрядностью) типу. А захардкодить подсмотренное значение -- моветон.

уж лучше что-нибудь типа такого заюзать

template<class T> auto as_signed (T t){ return make_signed_t <T>(t); }

Спасибо за комментарии!

Хочу кое что уточнить. То что я писал выше относится к стандарту C++17. В C++20 приняли "two's complement". Это раздел 6.8.1, параграф 3. То есть код из начала статьи:

uint32_t width = 7;
int32_t signed_offset = -width;

должен всегда работать одинаково и signed_offset в C++20 должно быть -7, как я понимаю.

В C++20 изменились правила преобразования целых https://timsong-cpp.github.io/cppwp/n4861/conv.integral#3. Думаю, в нашем случае следует понимать так: в результате преобразования получим знаковое целое, сравнимое с беззнаковым исходным по модулю 2^32. То есть такое знаковое определено единственным образом - это -7.

Кстати, значит, приведение больше не implementation defined, и мы можем не приводить руками.

Кстати, значит, приведение больше не implementation defined, и мы можем не приводить руками

Кажется, что так начиная с С++20. Я писал статью полагаясь на стандарт С++17, и не досмотрел, что-то поменяется в стандарте C++20 :)

С другой стороны, пока есть вероятность, что код могут скомпилировать, компилятором C++17, я бы не поленился и привел руками.

Это все понятно, но было бы интересно увидеть живой пример, когда будет не -7.

Польза выводов мне кажется немного не очевидной. Выражение s = -u для знакового s и беззнакового u состоит из двух: унарного минуса и присваивания.

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

8.5.2.1.8 ... The negative of an unsigned quantity is computed by subtracting its value from 2^n, where n is the number of bitsin the promoted operand. The type of the result is the type of the promoted operand.

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

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

7.8.3 If the destination type is signed, the value is unchanged if it can be represented in the destination type;otherwise, the value is implementation-defined.

То есть, как обычно, следует обращать внимание именно на совместимость данных разных типов.

Все так, но ведь в примере в выводах я предложил написать `int32_t signed_offset = -signed_width;`, где и signed_offset и signed_width знаковые типы. Как раз это нам и позволит уйти от unspecified behaviour, насколько я понимаю. Тогда 8.5.2.1.8 применен не будет. А 7.8.3 будет задействован в строчке выше: `int32_t signed_width = width;` в части value is unchanged.

@code_panik верно ли я понял вашу идею? Если нет, уточните п-та.

Вот пример из выводов:

uint32_t width = 7;
int32_t signed_width = width;
int32_t signed_offset = -signed_width;

Всё просто. Либо рабочий код буквально повторяет пример, тогда signed_offset = -7. Либо signed_offset нужно получить из произвольного сложного выражения без знака, тогда, по-видимому, единственное решение - приводить вручную все достаточно большие значения без знака к отрицательным со знаком.

Sign up to leave a comment.