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

时间:2015-07-21 16:47:25

标签: c gcc c-preprocessor integer-promotion

注意: 请参阅下面的编辑内容。

原始问题:

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

#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的回答中的评论,我的困惑最初源于表达式,其中包含LLL指定的常量{1}}后缀。我原始问题中包含的示例代码太简单了。这是一个更好的例子:

#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&#39;成功,包含受控文本;否则会被跳过。

&#34; 似乎隐含的内容以编译器已知的最宽整数类型执行所有计算&#34;是操作数本身被视为被指定为同样最宽的&#39;类型。换句话说,-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文档的段落,声明预处理器&#34;以编译器已知的最宽整数类型执行所有计算&#34;

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

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

编辑#3: 注意:我已将评论部分中的以下段落复制到帖子本身,因为有太多评论可供查看。< / em>的

  

如果有人可以在此主题上显示任何明确且相关的文字,   无论是标准文件还是GCC文档,我都感兴趣。

这里是6.10.1中的一些文字:

  
      
  1. 出于此令牌转换和评估的目的,所有带符号的整数类型和所有无符号整数类型的表现形式分别与 intmax_t uintmax_t <的类型相同/ strong>在标题&lt; stdint.h &gt;。
  2. 中定义   

这似乎会成功。

3 个答案:

答案 0 :(得分:4)

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

  

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

你的情况也是如此。

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

答案 1 :(得分:2)

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

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

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

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

答案 2 :(得分:1)

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

  

由此产生的标记构成控制常数表达式,根据[Section] 6.6的规则进行评估。

(C99,第6.10.1节)

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

  

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

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