为什么这个左移操作的结果会被认为是未定义的?

10

我同时使用C90和C99(由于某些原因,我最好不要讨论这些原因,因为它们会让我的血压升高并危及阻止我们将代码库移动到当前千年的人的生命)。尽管如此,我还是会引用C99标准。

我有类似以下紧凑形式的代码(test.c):

#include <stdio.h>

unsigned int foo(unsigned int n)
{
    unsigned int x, y;
    n = n - 264;
    x = (n >> 2) + 1;
    y = 1U << (x + 2U);
    return y;
}

int main(void)
{
    printf("%u\n", foo(384));
    return 0;
}
当然,传递给 foo() 的值可以比此处给出的值更大。但是384是最低值,将触发Clang静态分析器(从发布标签编译的3.4版本)发出警告。
$ clang -cc1 -triple x86_64-unknown-linux-gnu -analyze -analyzer-checker=core -internal-isystem /usr/local/include -internal-isystem $HOME/bin/LLVM/bin/../lib/clang/3.4/include -internal-externc-isystem /include -internal-externc-isystem /usr/include -O0 -x c test.c
test.c:8:9: warning: The result of the '<<' expression is undefined
        y = 1U << (x + 2U);
            ~~~^~~~~~~~~~~
1 warning generated.

现在逐行查看代码:

// n == 384
n = n - 264;        // n := 384 - 264
// n == 120
x = (n >> 2) + 1;   // x := (120 div 4) + 1
// x == 31
y = 1U << (x + 2U); // y := 1 << 33
所以,它将整数中的所有有意义的位都推出来了,根据我对以下内容的理解(来自 这里),这应该只会给我简单的零:

6.5.7 位移操作符

...

4

E1 << E2 的结果是E1左移E2个位位置; 腾空的位被填充为0。如果 E1 具有无符号类型,则结果的值为 E1 × 2^E2,取模于比结果类型中可表示的最大值多一个。如果 E1 具有带符号类型且非负值,并且 E1 × 2^E2 可在结果类型中表示,则该值即为结果;否则,行为未定义。

从我的阅读中,如果涉及到有符号值,那么未定义的结果只可能发生一次。但是,我确保所有值都是无符号的,甚至在文字上也明确说明了。
我错了还是Clang静态分析器过于热衷?
此代码的原始版本来自Jonathan Bennetts JB01 C++实现(版本1.40a)。

2
这不仅是未定义行为(正如其他人正确解释的那样);由于编译器或架构之间的差异,这种特定的未定义行为经常导致真正的错误。这不是语言中“虽然在形式上未定义,但在实践中通常能够正常工作”的角落之一。 - Stephen Canon
因为它回答了您的问题,对于该问题的答案实际上相当深入,包括a)标准和b)实际行为(x86 / x86-64很奇怪)。 @0xC0000022L - Mgetz
2个回答

7
在C99标准中,在您引用的部分之前:
整数提升将在每个操作数上执行。结果的类型是晋升的左操作数的类型。如果右操作数的值为负或大于或等于晋升的左操作数的宽度,则行为是未定义的。
今天大多数机器上的无符号整数有32位,这使得左移33位是未定义的行为。

3
同一段落在您引用的部分之前,即第6.5.7.3段中还提到:

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

因此,clang做得很好,因为一旦您移位的位数超过了提升后左操作数所能容纳的位数,行为确实是未定义的。

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