【问题标题】:Promotion when evaluating constant integer expressions in preprocessor directives - GCC在预处理器指令中评估常量整数表达式时的提升 - GCC
【发布时间】:2015-07-21 16:47:24
【问题描述】:

注意:请在下方查看我的编辑。

原始问题:

遇到了一些我无法调和的奇怪行为:

#if -5 < 0
#warning Good, -5 is less than 0.
#else
#error BAD, -5 is NOT less than 0.
#endif

#if -(5u) < 0
#warning Good, -(5u) is less than 0.
#else
#error BAD, -(5u) is less than 0.
#endif

#if -5 < 0u
#warning Good, -5 is less than 0u.
#else
#error BAD, -5 is less than 0u.
#endif

编译时:

$ gcc -Wall -o pp_test.elf pp_test.c
pp_test.c:2:6: warning: #warning Good, -5 is less than 0.
pp_test.c:10:6: error: #error BAD, -(5u) is less than 0.
pp_test.c:13:9: **warning: the left operand of "<" changes sign when promoted**
pp_test.c:16:6: error: #error BAD, -5 is less than 0u.

这表明预处理器在计算常量整数表达式时遵循不同的类型提升规则。即,当运算符具有混合符号的操作数时,将有符号操作数更改为无符号操作数。在 C 中(通常)正好相反。

我在文献中找不到任何支持这一点的东西,但有可能(可能?)我不够彻底。我错过了什么吗?这种行为是否正确?

就目前而言,似乎 #if 或 #elif 指令中涉及显式无符号整数常量的任何条件表达式都可能无法按预期运行,即与在 C 中一样。


编辑: 根据我在 Sourav Ghosh 的回答中的 cmets,我的困惑最初源于表达式,其中包含使用 LLL 后缀指定的常量。我在原始问题中包含的示例代码过于简化。这是一个更好的例子:

#if -5LL < 0L
#warning Good, -5LL is less than 0L.
#else
#error BAD, -5LL is NOT less than 0L.
#endif

#if -(5uLL) < 0L
#warning Good, -(5uLL) is less than 0L.
#else
#error BAD, -(5uLL) is less than 0L.
#endif

#if -5LL < 0uL
#warning Good, -5LL is less than 0uL.
#else
#error BAD, -5LL is less than 0uL.
#endif

建筑:

$ gcc -Wall -o pp_test.elf pp_test.c
pp_test.c:2:6: warning: #warning Good, -5LL is less than 0L.
pp_test.c:10:6: error: #error BAD, -(5uLL) is less than 0L.
pp_test.c:13:9: warning: the left operand of "<" changes sign when promoted
pp_test.c:16:6: error: #error BAD, -5LL is less than 0uL.

这似乎违反了 Sourav Ghosh 发布的 6.3.1.8 中的条款(我的重点):

否则,如果带符号整数类型的操作数的类型可以表示 无符号整数类型的操作数类型的所有值,然后 将无符号整数类型的操作数转换为 带符号整数类型的操作数

似乎违反了这个条款,因为-5LL 的排名高于0uL,并且因为第一个 (signed long long) 的类型确实可以代表所有值第二个(unsigned long)的类型。问题是,预处理器不知道这一点。

正如https://gcc.gnu.org/onlinedocs/gcc-3.0.2/cpp_4.html 中提到的(我的重点):

预处理器计算表达式的值。 它以编译器已知的最宽整数类型执行所有计算;在 GCC 支持的大多数机器上,这是 64 位。 这与编译器用于计算常量表达式的值的规则不同,在某些情况下可能会给出不同的结果。如果该值不为零,则 `#if' 成功并包含受控文本;否则跳过。

以编译器已知的最宽整数类型进行所有计算”似乎暗示的是,操作数本身被视为被指定为相同的“最宽”类型.换句话说,-5-5L 被视为-5LL0u0uL 被视为0uLL。这激活了 Sourav Ghosh 引用的子句,并导致观察到的行为。

实际上,就预处理器而言,只有一个等级,因此依赖于具有不同等级的操作数的类型提升规则将被忽略。这与编译器评估表达式的方式确实没有什么不同吗?


编辑#2:这是一个真实的例子,说明预处理器和编译器对相同表达式的计算方式不同(取自 Optiboot)。

#ifndef BAUD_RATE
#if F_CPU >= 8000000L
#define BAUD_RATE   115200L
#elif F_CPU >= 1000000L
#define BAUD_RATE   9600L
#elif F_CPU >= 128000L
#define BAUD_RATE   4800L
#else
#define BAUD_RATE 1200L
#endif
#endif

#ifndef UART
#define UART 0
#endif

#define BAUD_SETTING (( (F_CPU + BAUD_RATE * 4L) / ((BAUD_RATE * 8L))) - 1 )
#define BAUD_ACTUAL (F_CPU/(8 * ((BAUD_SETTING)+1)))
#define BAUD_ERROR (( 100*(BAUD_ACTUAL - BAUD_RATE) ) / BAUD_RATE)

#if BAUD_ERROR >= 5
#error BAUD_RATE error greater than 5%
#elif (BAUD_ERROR + 5) <= 0
#error BAUD_RATE error greater than -5%
#elif BAUD_ERROR >= 2
#warning BAUD_RATE error greater than 2%
#elif (BAUD_ERROR + 2) <= 0
#warning BAUD_RATE error greater than -2%
#endif

volatile long long int baud_setting = BAUD_SETTING;
volatile long long int baud_actual = BAUD_ACTUAL;
volatile long long int baud_error = BAUD_ERROR;

void foo(void) {
  baud_setting = BAUD_SETTING;
  baud_actual = BAUD_ACTUAL;
  baud_error = BAUD_ERROR;
}

为 AVR 目标构建:

$ avr-gcc -Wall -c -g -save-temps -o optiboot_pp_test.elf -DF_CPU=8000000L optiboot_pp_test.c

注意F_CPU 是如何被指定为有符号常量的。

optiboot_pp_test.c:28:6: warning: #warning BAUD_RATE error greater than -2% [-Wcpp]
     #warning BAUD_RATE error greater than -2%

这按预期工作。检查目标文件:

      baud_setting = BAUD_SETTING;
   8:   88 e0           ldi     r24, 0x08       ; 8
   a:   90 e0           ldi     r25, 0x00       ; 0
   c:   a0 e0           ldi     r26, 0x00       ; 0
   e:   b0 e0           ldi     r27, 0x00       ; 0
  10:   80 93 00 00     sts     0x0000, r24
  14:   90 93 00 00     sts     0x0000, r25
  18:   a0 93 00 00     sts     0x0000, r26
  1c:   b0 93 00 00     sts     0x0000, r27
      baud_actual = BAUD_ACTUAL;
  20:   87 e0           ldi     r24, 0x07       ; 7
  22:   92 eb           ldi     r25, 0xB2       ; 178
  24:   a1 e0           ldi     r26, 0x01       ; 1
  26:   b0 e0           ldi     r27, 0x00       ; 0
  28:   80 93 00 00     sts     0x0000, r24
  2c:   90 93 00 00     sts     0x0000, r25
  30:   a0 93 00 00     sts     0x0000, r26
  34:   b0 93 00 00     sts     0x0000, r27
      baud_error = BAUD_ERROR;
  38:   8d ef           ldi     r24, 0xFD       ; 253
  3a:   9f ef           ldi     r25, 0xFF       ; 255
  3c:   af ef           ldi     r26, 0xFF       ; 255
  3e:   bf ef           ldi     r27, 0xFF       ; 255
  40:   80 93 00 00     sts     0x0000, r24
  44:   90 93 00 00     sts     0x0000, r25
  48:   a0 93 00 00     sts     0x0000, r26
  4c:   b0 93 00 00     sts     0x0000, r27

... 表明已分配预期值。即baud_setting得到8baud_actual得到111111baud_error得到-3

现在我们使用定义为无符号常量的 F_CPU 进行构建(在此目标上是惯例):

$ avr-gcc -Wall -c -g -save-temps -o optiboot_pp_test.elf -DF_CPU=8000000UL optiboot_pp_test.c 
optiboot_pp_test.c:22:6: error: #error BAUD_RATE error greater than 5%
     #error BAUD_RATE error greater than 5%

报告的误差量级错误,符号错误。

对目标文件的检查表明它与为 F_CPU 使用带符号值构建的文件相同。

现在这一切都不足为奇,因为预处理器将所有常量视为最宽整数类型的有符号或无符号变体。

令人惊讶的是,标准和 GCC 文档(我可以找到)都没有明确提到这一点。

是的,预处理器完全遵循计算操作数的 C 规则,但仅限于二元运算符的两个操作数具有相同等级的情况。我在标准中找不到任何文本说明预处理器将所有指定的有或没有LLL 的常量都视为LL 中指定的整数提升规则6.3.1.8 被强制执行,我在 GCC 文档中也找不到任何提及此行为的内容。最接近的是上面引用的 GCC 文档中的一段,指出预处理器“以编译器已知的最宽整数类型执行所有计算”

这并不(不应该)明确表示操作数被视为使用后缀指定,将它们指定为编译器已知的最宽整数类型。实际上,如果没有关于该主题的明确段落,我的期望是操作数将遵循相同的类型转换和整数提升规则,当编译器评估时,所有操作数都遵循这些规则。情况似乎并非如此。 暗示,基于上述测试,普通 C 整数提升规则的应用是在预处理器将操作数提升为最宽(有符号或无符号)整数之后编译器已知的类型。

如果有人可以从标准或 GCC 文档中显示有关此主题的任何明确且相关的文本,我很感兴趣。


编辑#3: 注意:我已将以下段落从 cmets 部分复制到帖子本身,因为 cmets 太多,无法看到。

如果有人可以就该主题显示任何明确且相关的文字, 无论是来自标准还是 GCC 文档,我都很感兴趣。

这是 6.10.1 中的一些文本:

  1. 出于此令牌转换和评估的目的,所有有符号整数类型和所有无符号整数类型的行为都好像它们分别具有与 intmax_tuintmax_t 在标头 stdint.h> 中定义。

这似乎很成功。

【问题讨论】:

  • “即,当一个运算符有混合符号的操作数时,有符号的操作数被改变为一个无符号的操作数。相反(通常)在 C 中是正确的。” – 不,在 C 语言中也是如此。在 C 语言中,如果将等宽的有符号和无符号整数用作算术运算符的操作数,则有符号整数将转换为无符号类型。
  • "... 可能无法按预期运行..." 不。它确实完全按预期运行。 IOW:正如标准定义的那样——当然,除非你的编译器坏了。
  • Sourav Ghosh,感谢您添加“类型”标签。
  • “这与编译器评估表达式的方式真的没有区别吗?”这取决于回答问题的水平。关键区别在于 pp 如何解释 constants。它计算涉及与常量对应的类型化值的表达式的方式与 C 计算涉及相同类型化值的相同表达式的方式完全相同。随心所欲地描述它。我已经更新了我的答案,以便在这方面更加准确。
  • “关键的区别在于 pp 如何解释常量。” - 但这一切都不同了,不是吗?查看我对原始问题的新修改。

标签: c gcc c-preprocessor integer-promotion


【解决方案1】:

引用C11 标准第 6.3.1.8 章中的常用算术转换规则(强调我的)。

否则,如果具有无符号整数类型的操作数的秩大于或 等于另一个操作数的类型的等级,然后操作数与 有符号整数类型转换为无符号操作数的类型 整数类型。

你的情况也是如此。

一般来说,如果你尝试执行一些涉及有符号和无符号类型的操作,两个操作数都会先被提升为无符号类型,然后操作才会发生。

【讨论】:

  • 原来如此。我熟悉 6.3.1.8。让我明白的是你省略的子句之一:“否则,如果带符号整数类型的操作数的类型可以表示无符号整数类型的操作数类型的所有值,则转换无符号整数类型的操作数为带符号整数类型的操作数的类型。”这个条款当然不适用于这里,因为它前面有你引用的条款。
  • 我忘记的是,当预处理器计算表达式时,all 整数常量操作数的等级是相同的,即等于所支持的最宽整数类型的等级编译器,一般是long long int。以前我一直在摆弄添加LLL 后缀,当然无济于事,然后显然被一些闪亮的东西分心了;-) 谢谢大家。
【解决方案2】:

阅读here 了解算术运算的整数转换,包括比较。

这基本上会导致-对于您的示例,您将相同等级的有符号和无符号混合在一起-将有符号转换为无符号表示,反之亦然。因此,后两者的比较是无符号的。这对于预处理器和实际编译器是相同的。

6.3.1.3p2,对于 2s 补码有符号表示(如今在标准 CPU 中最常见)意味着有符号整数值的二进制表示被简单地重新解释为无符号(正)值,因此比较都失败了。

请注意,您应该启用 -Wconversions (gcc) 以查看有关此类有问题的转换的警告。

【讨论】:

    【解决方案3】:

    预处理器对数值常量的的解释在某些极少数情况下可能与C的不同,这是将所有整数值视为最广泛的可用值的副作用有符号或无符号类型,视情况而定,与宽度说明符无关。然而,给定生成的类型数值,其评估条件表达式的规则与 C 的规则明确相同:

    生成的标记组成控制常量表达式,根据 [Section] 6.6 的规则进行评估。

    (C99,第 6.10.1 节)

    第 6.6 节介绍了 C 的常量表达式规则,其中(在第 11 段)

    常量表达式求值的语义规则与非常量表达式相同。

    因此,评估规则是相同的。特别是,当二元运算符的操作数类型不同时,在每种情况下都会应用相同的“通常的算术转换”。其他答案说明了这些细节。

    【讨论】:

    • The preprocessor's interpretation of numeric constants can differ from C's under certain rare circumstances - 结果并不罕见。请参阅我原来的问题中的修改。
    • @joeymorin,我所说的情况是,常量的实际值(而不是类型)被预处理器解释为与 C 不同。虽然这些情况是相对的易于故意探测,它们很少影响实际代码。它们不在您的特定代码中。
    猜你喜欢
    • 2016-08-21
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-12-26
    • 1970-01-01
    • 2018-03-22
    相关资源
    最近更新 更多