Является ли сдвиг по левому краю целочисленного поведения 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
ловит попытки сдвинуть левые отрицательные числа.