首页 文章

在预处理程序指令中评估常量整数表达式时的升级 - GCC

提问于
浏览
4

NOTE: 请参阅下面的编辑 .

原始问题:

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

#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中 .


EDIT: 根据我在Sourav Ghosh的回答中的评论,我的困惑最初源于表达式,其中包括用 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'成功并包含受控文本;否则会被跳过 .

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

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


EDIT #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文档的段落,声明预处理器"carries out all calculations in the widest integer type known to the compiler" .

这不应该(不应该)明确表示将操作数视为用后缀指定它们,将后缀指定为编译器已知的最宽整数类型 . 实际上,如果没有关于该主题的明确段落,我的期望是操作数将受到编译器评估时所有操作数所适用的相同类型转换和整数提升规则的约束 . 似乎并非如此 . 基于上述测试,其含义是在预处理器将操作数提升为编译器已知的最宽(有符号或无符号)整数类型之后,应用正常的C整数提升规则 .

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


EDIT #3: 注意:我已将评论部分中的以下段落复制到帖子本身,因为有太多评论可供查看 .

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

这是6.10.1中的一些文字:

出于此令牌转换和求值的目的,所有带符号的整数类型和所有无符号整数类型的行为就像它们分别与头文件<stdint.h>中定义的intmax_t和uintmax_t类型相同 .

这似乎会成功 .

3 回答

  • 3

    引用通常的算术转换规则,(强调我的)来自 C11 标准,章节§6.3.1.8 .

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

    你的情况也是如此 .

    通常,如果您尝试执行涉及有符号和无符号类型的某些操作,则两个操作数将首先提升为无符号类型,然后执行操作 .

  • 1

    阅读here关于算术运算的整数转换,包括比较 .

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

    6.3.1.3p2,对于2s补码签名表示(现在最常用于标准CPU)意味着有符号整数值的二进制表示只是被重新解释为无符号(正)值,因此比较都失败了 .

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

  • 1

    在某些罕见的情况下,预处理器对数值常量值的解释可能与C不同,因为无论宽度说明符如何,将所有整数值视为最宽的可用有符号或无符号类型的副作用 . 但是,给定生成的类型化数值,其评估条件表达式的规则明确与C的相同:

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

    (C99,第6.10.1节)

    第6.6节介绍C的常数表达规则,其中(第11段)

    用于评估常量表达式的语义规则与非常量表达式相同 .

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

相关问题