целые числа со знаком теперь ведут себя по-другому в отношении сдвига влево?

В C++20 целые числа со знаком теперь определяются с использованием дополнения до двух,
см. http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2018/p0907r3..html

Это долгожданное изменение, однако один из пунктов списка привлек мое внимание:

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

Это кажется странным изменением. Не сместит ли это бит знака?


person sp2danny    schedule 18.01.2019    source источник


Ответы (2)


Формулировка C++17 для знаковых сдвигов влево (E1 << E2 ) был:

В противном случае, если E1 имеет знаковый тип и неотрицательное значение, а E1×2E2 может быть представлено в соответствующем беззнаковом типе результирующего типа, то это значение, преобразованное в результирующий тип, является результирующее значение; в противном случае поведение не определено.

Обратите внимание, что здесь говорится о возможности представления в «соответствующем типе unsigned». Таким образом, если у вас есть 32-разрядное целое число со знаком, значение которого равно 0x7FFFFFFF, и вы сдвинете его влево на 1, результирующий сдвиг можно представить в виде 32-разрядного целого числа без знака (0xFFFFFFFE). Но затем это беззнаковое значение преобразуется в тип результата. А преобразование целого числа без знака, значение которого слишком велико для соответствующего типа со знаком, определяется реализацией.

В целом, в С++ 17 сдвиг влево к знаковому биту может происходить через поведение, определяемое реализацией, и даже в этом случае только в том случае, если вы не выходите за пределы размера беззнакового типа результата. Проходя мимо, это явно UB.

Формулировка C++20 для целых чисел со знаком и без знака, является:

Значение E1 ‹‹ E2 — это уникальное значение, соответствующее E1×2E2 по модулю 2N, где N — ширина типа результата.

Целочисленное соответствие по модулю числа в основном означает отсечение битов за пределами числа по модулю. «Ширина» целого числа явно определяется как:

Диапазон представляемых значений для целочисленного типа со знаком составляет от −2N−1 до 2N−1−1 (включительно), где N называется шириной типа. .

Это означает, что для 32-битного целого числа со знаком ширина равна 31. Таким образом, модуль результата сдвига равен 31 биту, что отрезает бит знака, явно предотвращая сдвиг в него.

Итак, в C++20 у нас более жесткая гарантия; реализации не могут никогда выполнять знаковый сдвиг влево в знаковый бит. Это отличается от C++17 только в том смысле, что дисперсия реализации/UB явно определена так, чтобы не происходить.

Таким образом, сдвиг влево не был определен для перехода к знаковому биту в C++17 и определен, чтобы не делать этого в C++20.

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

person Nicol Bolas    schedule 18.01.2019
comment
Я не понимаю ваше последнее предложение. Сдвиг влево не сместился в знаковый бит, и он до сих пор не смещается.. Когда вы сдвигаете влево, скажем, int со значением INT_MAX, вы переходите в знаковый бит и результат это -2. AFAIUI до C++20 это было неопределенное поведение, а теперь нет. - person maxschlepzig; 15.03.2020
comment
@maxschlepzig: я отредактировал ответ, чтобы лучше объяснить ситуацию, с большим количеством цитат. Тот же ответ, только более подробно. - person Nicol Bolas; 15.03.2020
comment
Ширина (типичная) signed составляет 32; в показателях есть -1. Итак, INT_MAX/2+1<<1 == INT_MIN, поскольку -2*31 сравнимо с 2^31 по модулю 2^32, и INT_MIN<<2 == 0. Логика заключается в том, что битовые операции применяются к целым числам как к битовым последовательностям, независимо от особого значения знаковых битов, или, альтернативно, операция просто выполняется при естественном изоморфизме между целыми числами со знаком и без знака. - person Davis Herring; 15.03.2020
comment
Хорошо, я должен исправить свой последний комментарий: это было определено реализацией. И я согласен с комментарием @DavisHerring, ширина 32-битного целого числа со знаком равна 32, то есть вы должны установить N=32. См. также С++ 17, раздел 6.8.1, параграф 1, который вы уже цитируете. См. также следующий абзац: «Целочисленный тип без знака имеет ту же ширину N, что и соответствующий целочисленный тип со знаком». Таким образом, остальная часть вашего ответа не выполняется. - person maxschlepzig; 15.03.2020

Да, поведение целого числа со знаком со сдвигом влево изменилось с C++20.

В C++17 сдвиг влево положительного целого числа со знаком в знаковый бит вызывает поведение определяемое реализацией.1 Пример:

int i = INT_MAX;
int j = i << 1;    // implementation defined behavior with std < C++20

C++20 изменил это поведение на определенное, поскольку оно предписывает дополнение до двух представление целых чисел со знаком.2,3

В C++17 сдвиг отрицательного целого числа со знаком вызывает поведение undefined.1 Пример:

int i = -1;
int j = i << 1;    // undefined behavior with std < C++20

В C++20 это также изменилось, и теперь эта операция также вызывает определенное поведение.3

Это кажется странным изменением. Не сместит ли это бит знака?

Да, знаковый сдвиг влево сдвигает бит знака. Пример:

int i = 1 << (sizeof(int)*8-1);    // C++20: defined behavior, set most significant bit
int j = i << 1;                    // C++20: defined behavior, set to 0 

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

В настоящее время, поскольку все ЦП реализуют дополнение до двух, естественно, что стандарт C++ предписывает его. И если вы предписываете дополнение до двух, то только последует то, что вы сделаете вышеописанные операции определенным поведением, потому что это также то, как сдвиг влево ведет себя во всех архитектурах набора инструкций с дополнением до двух (ISA).

IOW, оставив его реализацию определенной и неопределенной, ничего вам не купит.

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


1

Значение E1 << E2 — это битовые позиции E1 со сдвигом влево E2; освободившиеся биты заполняются нулями. Если E1 имеет беззнаковый тип, значение результата равно E1 × 2**E2, уменьшенному по модулю на единицу больше, чем максимальное значение, представленное в типе результата. В противном случае, если E1 имеет тип со знаком и неотрицательное значение, а E1 × 2**E2 представляется в соответствующем беззнаковом типе типа результата, то это значение преобразуется в результат тип — результирующее значение; иначе поведение не определено.

(Окончательная версия C++17 черновик, раздел 8.8 Операторы сдвига [expr.shift], параграф 2, стр. 132 — выделение мое)

2

[..] Для каждого значения x целочисленного типа со знаком значение соответствующего целочисленного типа без знака, конгруэнтное x по модулю 2 N, имеет то же значение соответствующих битов в его представлении значения. 41) Это также известно как представление с дополнением до двух. [..]

(C++20 последняя рабочая черновик, Раздел 6.8.1 Основные типы [basic.fundamental], Параграф 3, стр. 66)

3

Значение E1 << E2 – это уникальное значение, конгруэнтное E1 × 2**E2 modulo 2**N, где N – ширина тип результата. [Примечание: E1 — это битовые позиции E2, сдвинутые влево; освободившиеся биты заполняются нулями. — примечание в конце]

(C++20 последняя рабочая черновик, раздел 7.6.7 Операторы сдвига [expr.shift], параграф 2, стр. 129, ссылка моя)

person maxschlepzig    schedule 16.03.2020
comment
См. также мой ответ на связанный вопрос. - person maxschlepzig; 16.03.2020