【问题标题】:Why this constant expression is not constant为什么这个常量表达式不是常量
【发布时间】:2019-11-13 07:19:24
【问题描述】:

我有以下 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]

这是为什么呢?这是编译器错误吗?显然,constant 是用常量表达式初始化的。

【问题讨论】:

  • @PM77-1:这不是问题所在。问题不在于表达式constant 是否是一个常量表达式,而在于它的初始化器是否是一个常量表达式。
  • 会不会是某个子表达式超出了int的范围? (请注意,clang 不会抱怨。)可能是 gcc 错误。
  • @vaigult 我无法重现错误。
  • 您有 UB 是由于左移溢出导致的,因此此示例不演示编译器错误。但是,我不确定这是否完全解释了这个问题。如果您将(1 &lt;&lt; 6) 更改为(1 &lt;&lt; 5),GCC 9.1 是否仍会拒绝该代码?
  • @JohnBollinger:对我来说,如果我将第一个(唯一的)(1 &lt;&lt; 6) 更改为 (1 &lt;&lt; 5),错误消息就会消失。当您将 6 更改为 5 时,该术语变为 ((((1 &lt;&lt; 5) - 1) &lt;&lt; ((((0 + 8) + 8) + 3) + 7)) &amp; ((1) &lt;&lt; ((((0 + 8) + 8) + 3) + 7))),它避免将 1 向左移动 32(总共向左移动 31)。这可能是编译器的“好消息”和“糟糕消息”的混合。

标签: c gcc


【解决方案1】:

我收到此错误:

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

这是为什么呢?这是编译器错误吗?显然,常量被初始化 使用常量表达式。

“常量表达式”在语言标准中是a defined term。我怀疑 GCC 正在以这种方式使用它,因为标准确实要求您的初始化程序在这个意义上是一个常量表达式。当然,您的代码的评估需要在这种情况下进行。

常量表达式有两种语言限制:

常量表达式不应包含赋值、自增、 减量、函数调用或逗号运算符,除非它们是 包含在未计算的子表达式中。

每个常量表达式都应计算为一个常量,该常量位于 其类型的可表示值范围。

前者对你来说不是问题。然而,后者在类型 int 具有 31 个或更少值位的 C 实现中的问题(包括大多数平台上的 GCC)。特别考虑这个子表达式:

(((1 << 6) - 1) << ((((0 + 8) + 8) + 3) + 7))

...但是为了理智,让我们删除一些不必要的括号并简化外部&lt;&lt; 的右侧以获得它,它保留了相关特征:

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

所有单个数字常量的类型为int,因此所有中间结果也是如此(其中简化版本中的“26”对应于原始表达式中的这种中间结果)。该左移的算术正确结果需要至少 32 个值位,并且因为您的 int(可能)没有那么多,因为为符号保留了一位,所以行为是未定义的。

因此,这里没有编译器错误,尽管您可能有理由抱怨实施质量。同样,没有任何编译器在没有警告或错误的情况下接受代码,因为这个原因是错误的。在另一种意义上,您的代码确实违反了语言约束,从这个意义上说,编译器有义务发出诊断,尽管它选择的那个似乎具有误导性。

此外,其他人在这个问题上的 cmets 似乎确认溢出与错误相关,因为将调用表达式从使用 (1 &lt;&lt; 6) 更改为 (1 &lt;&lt; 5)(1u &lt;&lt; 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;
}

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

【讨论】:

  • Tl;dr 为什么会产生 UB 的表达式会影响编译器输出?那是 UB 案件的一部分吗?
  • 错误信息是正确的,但可以通过解释为什么表达式不是常量表达式(在这种情况下,子表达式溢出)来改进。
  • @machine_1,一般来说,“未定义的行为”适用于程序和C实现的组合。它可以在翻译(编译)时或运行时显示。某些未定义的行为仅在运行时才有意义——例如,如果它们取决于外部提供的数据——但情况并非如此。
猜你喜欢
  • 1970-01-01
  • 2014-08-26
  • 1970-01-01
  • 2011-11-15
  • 1970-01-01
  • 2019-06-22
  • 1970-01-01
  • 2022-10-08
  • 2015-08-27
相关资源
最近更新 更多