Является ли сдвиг по левому краю целочисленного поведения undefined в С++ 03?

Согласно С++ 03, 5.8/2, сдвиг влево определяется следующим образом:

Значение E1 < E2 - E1 (интерпретируется как битовый шаблон), сдвинутые слева позиции E2; освобожденные биты заполняются нулями. Если E1 имеет неподписанный тип, значение результата E1 умножается на величину 2, поднятую до мощности E2, уменьшенную по модулю ULONG_MAX + 1, если E1 имеет тип unsigned long, UINT_MAX + 1 в противном случае.

Меня беспокоит то, что неподписанные типы явно упоминаются, но подписанные типы полностью игнорируются. Сравните это с 5.8/3, который определяет правое смещение:

Значение E1 → E2 - это позиции E1 с правым сдвигом E1. Если E1 имеет неподписанный тип, или если E1 имеет подписанный тип и неотрицательное значение, значение результата является неотъемлемой частью частного E1, деленной на величину 2, поднятую до мощности E2. Если E1 имеет подписанный тип и отрицательное значение, результирующее значение определяется реализацией.

В 5.8/3 указаны как подписанные, так и unsigned, даже подписанные с неотрицательными и подписанными отрицательными значениями, упомянутыми отдельно.

AFAIK, когда что-то явно не определено в стандарте С++, поведение undefined. Я также видел этот вопрос, но он фокусируется на различиях между C и С++ и, похоже, не имеет ответа, на который все согласятся.

Является ли сдвиг слева целочисленным знаком, определенным в С++ 03?

Ответ 1

5.8/2 говорит, что он интерпретирует его как бит-шаблон, который зависит только от реализации, если по какой-то причине ваша реализация не использует 2 дополнения, или если ваш компилятор предпочел вам (они этого не делают). С++ 11 более явный, но говорит то же самое.

Подписанные целые числа используют то, что известно как 2 дополнения. В основном, если вы смещаете бит целое число со знаком на 1, если оно положительное и ниже 2 ^ (бит-2), оно будет работать так, как будто оно было без знака. Если он выше этого, но положительный, вы создадите странное отрицательное число, которое не имеет отношения к оригиналу. Если это отрицательно для начала, вы получите возможно отрицательное, возможно, положительное число.

Например, если у нас есть 8-разрядное целое число со знаком, представляющее -1:

11111111 // -1

Если мы оставили сдвиг, в результате получим

11111110 // -2

Однако, скажем, имеем -120

10001000  // -120

В итоге получим

00010000  // 16

Очевидно, что это неверно!

Продолжая, используя номер 65:

01000001  // 65

Сдвиг влево, это станет следующим:

10000001  // -127

Что соответствует -127.

Однако число 16:

00010000 // 16

Сдвиг слева -

00100000 // 32

Как вы можете видеть, он "иногда работает, иногда не работает", но обычно работает, если ваш номер меньше 2 ^ (бит-2), а иногда, но не обычно, если он выше - (2 ^ (бит-2 )). То есть сдвинуть влево на 1. Чтобы сдвинуть влево на 2, отбросьте еще один бит. Etc.

Ответ 2

Я хотел бы добавить, что правила были изменены в С++ 11.

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

Режим Clang -fsanitize=undefined ловит попытки сдвинуть левые отрицательные числа.