右移运算符的奇怪行为(1 >> 32)

22

最近我遇到了使用右移位运算符时的一种奇怪行为。

下面是示例程序:

#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
我只是想知道,为什么会被投票为“不是一个真正的问题”?! - ereOn
1
非常有趣的问题...即使我认为通过一个大数旋转会导致0。从未知道它是UB。 - Naveen
3
讨厌者总是讨厌。 :) 这也是恶意投票的唯一解释。 - tzaman
7个回答

38

很可能是CPU正在进行计算。

a >> (b % 32)

foo 中,同时 1 >> 32 是一个常量表达式,因此编译器将在编译时折叠这个常量,从而得到0。

由于标准(C++98 §5.8/1)规定:

如果右操作数为负数或大于等于左操作数的升级后长度的位数,则行为未定义。

因此,foo(1,32)1>>32 得出不同结果是没有矛盾的。

 

另一方面,在 bar 中,您提供了一个64位无符号值,因为 64 > 32,所以结果必须是 1 / 232 = 0。然而,如果您编写:

bar(1, 64);

即使这样,你仍然可能得到 1。


编辑:逻辑右移(SHR)在x86/x86-64上的行为类似于 a >> (b % 32/64)(Intel #253667第4-404页):

目标操作数可以是寄存器或内存位置。计数操作数可以是立即值或CL寄存器。计数被掩码为5位(如果在64位模式下使用REX.W,则为6位)。计数范围限制为0到31(如果在64位模式下使用REX.W,则为63)。对于计数为1,提供了一种特殊的操作码编码。

然而,在ARM(armv6&7中,至少),逻辑右移(LSR)的实现方式为(ARMISA第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页面AppxB-13)

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

这保证了右移大于等于32位将产生0。例如,在iPhone上运行此代码时,foo(1,32)将返回0。

这表明将一个32位整数向右移动大于等于32位是不可移植的。


1
谢谢。我一直以为位移“太多”会将值归零,而不是UB。所以我猜在这里使用64位值(右移操作数为32)既正确又有定义的行为? - ereOn
3
b % 32 看起来正确;我尝试了一个 foo(16, 33),得到的结果是 8。干得好! - tzaman
非常好的答案;清晰完整的解释。再次感谢。 - ereOn

6

好的,所以它在5.8.1中:

操作数必须是整型或枚举类型,并执行整数提升。结果的类型是提升后的左操作数的类型。如果右操作数为负数或大于或等于提升后的左操作数的位长度,则行为未定义。

所以你有一个未定义的行为(Undefined Behaviour™)。


3
在foo函数中,移位宽度大于或等于被移位数据的大小。在C99标准中,这会导致未定义的行为。很可能在MS VC++所构建的任何C++标准中都是如此。
这样做的原因是为了让编译器设计者利用CPU硬件对移位的支持。例如,i386架构有一条指令可以将32位字向左或向右移动指定数量的位数,但位数是由指令中一个5位宽度的字段来定义的。最可能的情况是,您的编译器通过将您的位移量与0x1F进行掩码操作来生成该指令。这意味着移位32位与移位0位是相同的。

1

我使用VC9编译器在32位Windows上进行了编译。它给了我以下警告。由于我的系统编译器中sizeof(int)为4字节,因此编译器指示右移32位会导致未定义的行为。由于它是未定义的,您无法预测结果。只是为了检查,我使用31位进行了右移,所有警告都消失了,结果也如预期一样(即0)。


未定义不等于不可预测。它可能意味着那样,但并不一定如此。 - Nathan Fellman
@Nathan 但出于实际原因,未定义通常应像不可预测一样处理。否则,代码将与特定的构建/运行时环境耦合。 - foraidt
这就是为什么英特尔和微软在向后兼容性方面都遇到了很多麻烦。软件经常(或足够频繁地)执行某些未定义的操作,只发现它在未来的CPU或操作系统上出现故障。用户不知道软件有问题,并认为是英特尔或微软的问题,导致负面报道。即使旧代码编写得很差,微软和英特尔也会尽最大努力不破坏遗留代码。 - Nathan Fellman

0

警告已经说明了一切!

但公平地说,我曾经也因为同样的错误而受到过伤害。

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

是完全未定义的。实际上,根据我的经验,编译器通常会给出与运行时不同的结果。我的意思是,如果编译器能够在运行时看到并评估移位表达式,它可能会给出与在运行时评估的表达式不同的结果。


0
我猜原因是因为int类型占用32位(对于大多数系统来说),但其中一位用于符号,因为它是有符号类型。所以实际值只使用了31个位。

有符号性是语言层面上的解释问题。CPU“看到”的是位,而不是值或符号。 - DevSolar

-5

foo(1,32)执行一个循环移位,因此应该消失的位重新出现在左侧。如果你这样做32次,设置为1的单个位将回到其原始位置。

bar(1,32)也是一样,但是位于第64-32+1=33位,这超出了32位int可表示的数字范围。只有32个最低位被取出,并且它们都是0。

1 >> 32由编译器执行。不知道为什么gcc在这里使用非旋转移位而不是生成的代码中。

((int)1 >> (int)32)同理。


4
这里的 >> 不是循环移位,否则 1 >> 1 将产生 INT_MIN,而它显然不会。问题在于,如果按照类型中的位数进行移位,则会导致未定义行为。在这种情况下,它表现为等同于循环移位的结果纯属巧合。 - Pavel Minaev

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接