21

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

以下程序:

#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
4

7 回答 7

37

CPU很可能实际上正在计算

a >> (b % 32)

foo; 同时, 1 >> 32 是一个常量表达式,因此编译器将在编译时折叠该常量,以某种方式给出 0。

由于标准 (C++98 §5.8/1) 声明

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

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, Page 4-404):

目标操作数可以是寄存器或内存位置。计数操作数可以是立即数或 CL 寄存器。计数被屏蔽为 5 位(如果在 64 位模式下使用 REX.W,则为 6 位)。计数范围限制为 0 到 31(如果使用 64 位模式和 REX.W,则为 63)。为计数 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 页 AppxB-13)

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

这保证了≥32 的右移将产生零。例如,当这段代码在 iPhone 上运行时,foo(1,32)会给出 0。

这些表明将 32 位整数移位 ≥32 是不可移植的。

于 2010-08-03T07:07:24.053 回答
6

好的。所以它在 5.8.1 中:

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

所以你有一个未定义的行为(tm)。

于 2010-08-03T07:11:43.067 回答
3

foo 中发生的情况是移位宽度大于或等于要移位的数据的大小。在导致未定义行为的 C99 标准中。在构建任何 C++ 标准 MS VC++ 时,它可能都是相同的。

这样做的原因是允许编译器设计人员利用任何 CPU 硬件对移位的支持。例如,i386 体系结构有一条指令将 32 位字移位若干位,但位数是在指令中 5 位宽的字段中定义的。最有可能的是,您的编译器通过获取位移量并用 0x1F 对其进行掩码来生成指令,以获取指令中的位移位。这意味着移位 32 与移位 0 相同。

于 2010-08-03T07:50:31.617 回答
1

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

于 2010-08-03T07:13:30.443 回答
0

警告说明了一切!

但公平地说,我曾经被同样的错误所困扰。

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

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

于 2010-08-03T07:12:44.790 回答
0

我想原因是该int类型拥有 32 位(对于大多数系统),但一位用于符号,因为它是有符号类型。所以只有 31 位用于实际值。

于 2010-08-03T07:07:54.917 回答
-5

foo(1,32) 执行旋转操作,因此应该在右侧消失的位重新出现在左侧。如果执行 32 次,则设置为 1 的单个位将返回其原始位置。

bar(1,32) 相同,但该位位于第 64-32+1=33 位,高于 32 位 int 的可表示数字。只取最低的 32 位,它们都是 0。

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

同样的事情 ((int)1 >> (int)32)

于 2010-08-03T07:14:29.507 回答