算术右移会产生错误的结果吗?

32

我一定是疯了,但是我机器上的gcc 4.7.3却给出了荒谬的结果。以下是我正在测试的精确代码:

#include <iostream>

using namespace std;

int main(){
  unsigned int b = 100000;
  cout << (b>>b) << endl;
  b = b >> b;
  cout << b << endl;
  b >>= b;
  cout << b << endl;
  return 0;
}

现在,任何一个数字如果被自己右移,应该会得到0 (n/(2^n) == 0 ,其中 n>1 ,且为正/无符号整数),但是一些情况下我的输出结果如下:

100000
100000
100000

我是疯了吗?到底发生了什么事?


@ShafikYagmour:这假设编译器会费心去加入这个指令。它完全有权利拒绝这个程序。 - MSalters
@MSalters,确实,我们现在涉及到编译器/平台/版本特定的问题,但对于当前和最近的版本来说,情况是这样的,正如我已经说明的那样,这是未定义的,所以你显然是自己负责的。只有在使用-O0时,gcc似乎才会产生一个shr - Shafik Yaghmour
@ShafikYaghmour:Intel只是GCC支持的众多平台之一,它们有不同的优化阶段。在优化中的一个常见技巧是说:“这个值只能在0到31之间,因为它用于移位,如果我按照代码路径X到达这里,该值将不在0到31之间,所以代码路径X是不可能的,我甚至不需要为其生成指令”。GCC以空指针检查而闻名。 - MSalters
@MSalters,这有道理,那么这也会影响警告吗? - Shafik Yaghmour
5
有趣的事实:虽然在C语言中这是未定义行为,但在C#中它是已定义行为,但定义有些奇怪。在C#中,对于32位整数x和y,x>>y的计算方式是x>>(y&0x1f)!因此,(x >> 16) >> 16为零,但x >> 32等于x。 - Eric Lippert
显示剩余3条评论
2个回答

49

在C++中,与C类似,移位操作的位数限制于被移位值的位数。例如,如果unsigned int是32位,则超过31位的移位是未定义的。

实际上,常见结果是使用移位量的最低5位,并忽略高阶位;这是由编译器产生一条完全这样做的机器指令所致(例如,在x86上进行SHR操作)。

在这种情况下,移位值为100000(十进制),它恰好是11000011010100000二进制-较低的5位为零。因此,你实际上获得了一个0位移。但是,您不应该依赖它;从技术上讲,您看到的是未定义行为

参考资料:

对于C语言,N1570第6.5.7节:

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

对于C ++,N3690第5.8节"[expr.shift]":

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

N1570是一份草案,几乎与发布的ISO C11标准相同;此条款自1989年ANSI C标准以来基本上没有改变。

N3690是C ++标准的最新草案;我不确定它是否是最好的用法,但是,这个条款没有改变。


1
你是否观察到这种行为在不同的编译器中出现过,或者是从某个地方读取的信息? - Grijesh Chauhan
3
@GrijeshChauhan,这在C和C++标准规范中有记录。关于SHR指令的生成,我已经观察到了。 - davmac
编程圣母啊...我刚测试了一下,100000的二进制是0b110000110101_00000。我试着将其左移100001位,结果它确实左移了1位。 - Suedocode
2
@Aggieboy 这也取决于其他因素,例如优化级别。gccclang在不同的优化级别下具有不同的输出,这完全符合未定义行为,即任何事情都可能发生。 - Shafik Yaghmour

32

如果您将位移大于左操作数的位长度,则会调用未定义行为draft C ++标准5.8Shift operators1段说(我强调):

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

有趣的是,如果位移量是文字,则gccclang可能为此代码生成警告:

cout << (b>> 100000) ;

如果b是一个const,那么gcc的警告如下:
warning: right shift count >= width of type [enabled by default]

根据MSalters在问题的评论中指出的,我们甚至可能不能依赖这个警告,因为这是未定义行为,这与标准中关于“未定义行为”的术语和定义部分的注释一致,该部分表示:

注意:[...] 可允许的未定义行为范围从完全忽略情况并产生不可预测的结果,到在翻译或程序执行期间以环境特征的记录方式行为(有或没有发出诊断消息),到终止翻译或执行(带有发出诊断消息)。[...]

平台特定细节

对于示例代码中似乎缺少移位的潜在解释是因为在某些平台上,移位计数将被掩码到5位,例如在x86架构上,我们可以在Intel® 64和IA-32体系结构软件开发人员手册中的SAL/SAR/SHL/SHR-Shift部分中看到,在IA-32体系结构兼容性部分中说:

8086不会掩码移位计数。但是,所有其他IA-32处理器(从Intel 286处理器开始)都会将移位计数掩码为5位,从而最多可计数31个。 [...]


80286故意允许包括字长在内的移位量。我想知道为什么80386没有这样做(当操作数大小为32位时使用6位CL)? - supercat
此外,有关x86的更多背景信息,请参阅此博客文章 - Shafik Yaghmour

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