Странное поведение оператора правого сдвига (1 >> 32)

Недавно я столкнулся с странным поведением с помощью оператора с правом сдвигом.

Следующая программа:

#include <cstdio>
#include <cstdlib>
#include <iostream>
#include <stdint.h>

int foo(int a, int b)
{
   return a >> b;
}

int bar(uint64_t a, int b)
{
   return a >> b;
}

int main(int argc, char** argv)
{
    std::cout << "foo(1, 32): " << foo(1, 32) << std::endl;
    std::cout << "bar(1, 32): " << bar(1, 32) << std::endl;
    std::cout << "1 >> 32: " << (1 >> 32) << std::endl; //warning here
    std::cout << "(int)1 >> (int)32: " << ((int)1 >> (int)32) << std::endl; //warning here

    return EXIT_SUCCESS;
}

Выходы:

foo(1, 32): 1 // Should be 0 (but I guess I'm missing something)
bar(1, 32): 0
1 >> 32: 0
(int)1 >> (int)32: 0

Что происходит с функцией foo()? Я понимаю, что единственная разница между тем, что он делает и последние две строки, заключается в том, что последние две строки оцениваются во время компиляции. И почему он "работает", если я использую целое число 64 бит?

Любые огни относительно этого будут очень признательны!


Несомненно, это то, что g++ дает:

> g++ -o test test.cpp
test.cpp: In function 'int main(int, char**)':
test.cpp:20:36: warning: right shift count >= width of type
test.cpp:21:56: warning: right shift count >= width of type

Ответы

Ответ 1

Вероятно, процессор фактически вычисляет

a >> (b % 32)

в foo; между тем, 1 → 32 является постоянным выражением, поэтому компилятор сбрасывает константу во время компиляции, которая как-то дает 0.

Поскольку стандарт (С++ 98 & sect; 5.8/1) утверждает, что

Поведение undefined, если правый операнд отрицательный или больше или равен длине в битах продвинутого левого операнда.

нет противоречия, когда foo(1,32) и 1>>32 дают разные результаты.

 

С другой стороны, в bar вы указали 64-битное значение без знака, так как 64 > 32 гарантированно, что результат должен быть 1/2 32= 0. Тем не менее, если вы написать

bar(1, 64);

вы все равно можете получить 1.


Изменить: логический сдвиг вправо (SHR) ведет себя как a >> (b % 32/64) на x86/x86-64 (Intel # 253667, Страница 4-404):

Операндом назначения может быть регистр или ячейка памяти. Оператор count может быть немедленным значением или регистром CL. Счет маскируется до 5 бит (или 6 бит, если используется в 64-битном режиме и используется REX.W). Диапазон отсчетов ограничен 0 - 31 (или 63, если 64-разрядный режим и REX.W). Для подсчета 1 предоставляется специальная кодировка кода операции.

Однако, по ARM (по крайней мере, armv6 & 7) логический сдвиг вправо (LSR) реализуется как (ARMISA Page A2-6)

(bits(N), bit) LSR_C(bits(N) x, integer shift)
    assert shift > 0;
    extended_x = ZeroExtend(x, shift+N);
    result = extended_x<shift+N-1:shift>;
    carry_out = extended_x<shift-1>;
    return (result, carry_out);

где (ARMISA Page AppxB-13)

ZeroExtend(x,i) = Replicate('0', i-Len(x)) : x

Это гарантирует, что сдвиг вправо ≥32 приведет к нулю. Например, когда этот код запускается на iPhone, foo(1,32) будет давать 0.

Это означает, что перенос 32-разрядного целого числа на ≥32 не переносится.

Ответ 2

OK. Итак, это в 5.8.1:

Операнды должны быть целочисленного или перечисляемого типа, и выполняются интегральные продвижения. Тип результата это продвинутый левый операнд. Поведение undefined, если правый операнд отрицательный или больше или равен длина в битах продвинутого левого операнда.

Итак, у вас есть undefined Behavior (tm).

Ответ 3

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

Причина этого заключается в том, чтобы позволить разработчикам компилятора использовать любую аппаратную поддержку процессора для смен. Например, архитектура i386 имеет команду сдвинуть 32-битное слово на количество бит, но количество бит определено в поле в инструкции, ширина которой составляет 5 бит. Скорее всего, ваш компилятор генерирует команду, беря вашу величину сдвига бит и маскируя ее с помощью 0x1F, чтобы получить сдвиг бит в инструкции. Это означает, что смещение на 32 совпадает с сдвигом на 0.

Ответ 4

Я скомпилировал его на 32-битных окнах, используя компилятор VC9. Это дало мне следующее предупреждение. Поскольку sizeof(int) составляет 4 байта, мой системный компилятор указывает, что смещение вправо на 32 бита приводит к поведению undefined. Поскольку это undefined, вы не можете предсказать результат. Просто для проверки, что я правильно сдвинулся с 31 битом, и все предупреждения исчезли, и результат был также ожидаемым (т.е. 0).

Ответ 5

Я полагаю, причина в том, что тип int содержит 32 бита (для большинства систем), но один бит используется для знака, так как он является подписанным типом. Таким образом, для фактического значения используются только 31 бит.

Ответ 6

Предупреждение говорит все!

Но, честно говоря, я однажды укусил одну и ту же ошибку.

int a = 1;
cout << ( a >> 32);

полностью undefined. Фактически, компилятор обычно дает разные результаты, чем время выполнения в моем опыте. Я имею в виду, что если компилятор сможет оценить выражение сдвига во время выполнения, он может дать вам другой результат для выражения, оцененного во время выполнения.

Ответ 7

foo (1,32) выполняет поворотное дерьмо, поэтому бит, который должен исчезнуть справа, снова появляется слева. Если вы делаете это 32 раза, единственный бит, установленный в 1, возвращается в исходное положение.

bar (1,32) то же самое, но бит находится в 64-32 + 1 = 33-й бит, который выше числа, представляемого для 32-битного int. Принимаются только 32 младших бит, и все они равны 0.

1 → 32 выполняется компилятором. Не знаю, почему gcc использует невращающийся сдвиг здесь, а не в сгенерированном коде.

То же самое для ((int) 1 → (int) 32)