为什么这个常量表达式不是常量

13

我有以下的C代码清单:

static const int constant = (0 | ((((1 << 6) - 1) << ((((0 + 8) + 8) + 3) + 7)) & ((1) << ((((0 + 8) + 8) + 3) + 7))) | ((((1 << 7) - 1) << (((0 + 8) + 8) + 3)) & ((0) << (((0 + 8) + 8) + 3))) | ((((1 << 3) - 1) << ((0 + 8) + 8)) & ((0) << ((0 + 8) + 8))) | ((((1 << 8) - 1) << 0) & ((1) << 0)));

int main(int argc, char** argv)
{
    return constant;
}

当我使用以下命令行尝试使用GCC-9.1编译它时:

gcc-9 -Werror -Wpedantic main.c

我遇到了这个错误:

main.c:1:29: error: initializer element is not a constant expression [-Werror=pedantic]

为什么会这样?这是编译器的一个bug吗?显然,constant使用了一个常量表达式进行了初始化。


2
@PM77-1:那不是问题所在。问题不在于表达式“constant”是否为常量表达式,而在于它的初始化器是否为常量表达式。 - Keith Thompson
1
可能是某个子表达式超出了 int 的范围吗?(请注意,clang 没有抱怨。)可能是 gcc 的一个 bug。 - Keith Thompson
1
@vaigult 我无法重现这个错误。 - Vlad from Moscow
4
由于左移溢出导致 UB,因此该示例并未证明编译器存在错误。然而,我不确定这是否完全解释了问题。如果将 (1 << 6) 更改为 (1 << 5),GCC 9.1 是否仍会拒绝该代码? - John Bollinger
3
如果我将第一个(也是唯一一个)(1 << 6) 改为 (1 << 5),错误消息就会消失。这个术语变成了 ((((1 << 5) - 1) << ((((0 + 8) + 8) + 3) + 7)) & ((1) << ((((0 + 8) + 8) + 3) + 7))),当你将6改为5时,避免了将 1 左移32位(总共左移31位)。编译器可能是在一些方面很好但某些方面则较差的混合体。 - Jonathan Leffler
显示剩余9条评论
1个回答

14

I am getting this error:

main.c:1:29: error: initializer element is not a constant expression [-Werror=pedantic]

Why is that? Is this a compiler bug? Clearly, constant is initialized with a constant expression.

"常量表达式"是语言标准中的定义术语。我怀疑GCC也是这样使用它的,因为标准确实要求您的初始化器在这个意义上是一个常量表达式。当然,需要以这种方式执行代码的评估。
常量表达式有两个语言约束:
常量表达式不得包含赋值、增量、减量、函数调用或逗号运算符,除非它们包含在不被评估的子表达式中。
每个常量表达式应该评估为其类型可表示值范围内的常量。
前者对您来说不是问题。然而,后者在C实现中存在问题,其中类型int具有31位或更少的值位(包括大多数平台上的GCC)。特别考虑此子表达式:
(((1 << 6) - 1) << ((((0 + 8) + 8) + 3) + 7))

...但是为了更加简化,让我们去掉一些不必要的括号并简化外部<<右侧的内容,以保留相关特性:

((1 << 6) - 1) << 26

所有的数字常量都有类型int,因此所有的中间结果也是如此(在简化版本中的“26”对应原始表达式中的这样一个中间结果)。那个左移的算术正确的结果需要至少32个值位,并且由于你的int(可能)没有那么多,因为一位被保留用于符号,所以行为未定义。
因此,这里没有编译器bug,尽管您可能对实现质量有投诉的理由。同样,任何接受代码而不发出警告或错误的编译器都不是因为存在错误。在不同意义上,您的代码确实违反了语言约束,在这种意义上,编译器有责任发出诊断,尽管它选择的那个似乎具有误导性。
此外,其他人对问题的评论似乎证实了溢出与错误有关,因为将被调用的表达式从使用(1 << 6)更改为(1 << 5)或(1u << 6)可以解决其他能够复制该问题的人的错误。两者都产生没有任何未定义行为的总体表达式。
请注意,当您进行位操作时,避免使用有符号整数类型通常是更好的选择。因此,在忽略从中提取此内容的更大程序的任何影响的情况下,我倾向于重写您的示例程序如下:
static const unsigned int constant = (0 
    | ((((1u << 6) - 1) << ((((0 + 8) + 8) + 3) + 7)) & ((1u) << ((((0 + 8) + 8) + 3) + 7)))
    | ((((1u << 7) - 1) << (((0 + 8) + 8) + 3))       & ((0u) << (((0 + 8) + 8) + 3)))
    | ((((1u << 3) - 1) << ((0 + 8) + 8))             & ((0u) << ((0 + 8) + 8)))
    | ((((1u << 8) - 1) << 0)                         & ((1u) << 0)));

int main(void) {
    // There's a potential issue with the conversion of the return value, too, but it
    // does not affect the particular expression at issue here.
    return constant;
}

注意,按位移操作的结果类型仅由其左操作数类型决定。

简而言之,为什么一个会产生未定义行为的表达式会影响编译器的输出?这是未定义行为案例的一部分吗? - machine_1
错误信息是正确的,但可以通过解释为什么表达式不是常量表达式(在这种情况下,子表达式溢出)来改进它。 - Keith Thompson
@machine_1,通常,“未定义行为”适用于程序和C实现的组合。它可以在编译时或运行时表现出来。某些未定义行为仅在运行时有意义--例如,如果它们取决于外部提供的数据--但这不是这种情况。 - John Bollinger

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