枚举常量在C和C ++中的行为不同


81

为什么这样做:

#include <stdio.h>
#include <limits.h>
#include <inttypes.h>

int main() {
    enum en_e {
        en_e_foo,
        en_e_bar = UINT64_MAX,
    };
    enum en_e e = en_e_foo;
    printf("%zu\n", sizeof en_e_foo);
    printf("%zu\n", sizeof en_e_bar);
    printf("%zu\n", sizeof e);
}

4 8 8用C和8 8 8C ++(在具有4字节整数的平台上)打印?

我的印象是,UINT64_MAX赋值将强制所有枚举常量至少为64位,但en_e_foo在纯C中仍为32位。

差异的理由是什么?


1
哪些编译器?我不知道这是否有所作为,但可能会有所不同。
Mark Ransom

@MarkRansom它是由gcc提出的,但是clang的行为相同。
PSkocik


3
“在具有4字节int的平台上”不仅平台,而且由编译器确定类型宽度。可能就是这些。(Per Keith的回答,实际上不是,但要总体上意识到这种可能性)
Lightness Races in Orbit

1
@PSkocik:并没有真正的改变,只是这个问题找到了cc ++的有效用法(询问为什么某些代码导致两者之间行为不同)。还可以:询问如何从C ++调用C库,以及如何编写可以从C调用的C ++。非常不正确:询问C问题并在“ C ++标记”上加上“以便获得更多关注”。同样也不行:问一个C ++问题,然后再想一想“确保您也为C回答”。(对于通常的抱怨者-不太好:将C ++标签更改为C标签,因为代码使用两种标准中都存在的功能)
Ben Voigt

Answers:


80

在C中,enum常数是类型int。在C ++中,它是枚举类型。

enum en_e{
    en_e_foo,
    en_e_bar=UINT64_MAX,
};

在C语言中,这是违反约束的行为,需要进行诊断(如果 UINT64_MAX超过INT_MAX,很可能会这样做)。AC编译器可能会完全拒绝该程序,或者可能会打印警告,然后生成其行为未定义的可执行文件。(尚不清楚100%违反约束的程序必然具有未定义的行为,但是在这种情况下,标准没有说明行为是什么,因此仍然是未定义的行为。)

gcc 6.2对此没有警告。lang做。这是gcc中的错误;使用标准标头中的宏时,它会错误地禁止某些诊断消息。感谢Grzegorz Szpetkowski定位错误报告:https ://gcc.gnu.org/bugzilla/show_bug.cgi?id=71613

在C ++中,每个枚举类型都有一个基础类型,它是某种整数类型(不一定是int)。此基础类型必须能够表示所有常量值。因此,在这种情况下,en_e_fooen_e_bar均为type en_e,即使int更窄也必须至少64位宽。


10
快速说明:UINT64_MAX不超过INT_MAX要求int至少为65位。
Ben Voigt

10
真正奇怪的是gcc(5.3.1)使用-Wpedantic和发出警告,18446744073709551615ULL但不使用发出警告UINT64_MAX
nwellnhof

4
@dascandy:否,int必须为带符号类型,因此必须至少为65位才能表示UINT64_MAX(2 ** 64-1)。
基思·汤普森

1
@ KeithThompson,6.7.2.2说:“枚举数列表中的标识符被声明为具有int类型的常量,并且可以在允许的任何位置出现。” 我的理解是,单个C枚举声明的常量不使用枚举的类型,因此从那里开始将它们设置为不同的类型不需要很大的工作量(尤其是将其实现为标准的扩展时)。
zneak

2
@AndrewHenle:en_e_bar不大于枚举,en_e_foo小于。枚举变量与最大常量一样大。
Ben Voigt

25

该代码首先不是有效的C语言。

C99和C11中的6.7.2.2节都说:

限制条件:

定义枚举常量值的表达式应为整数常量表达式,其值可表示为int

编译器诊断是强制性的,因为它违反了约束,请参阅5.1.1.3:

如果预处理翻译单元或翻译单元包含违反任何语法规则或约束的条件,则符合标准的实现也应至少产生一条诊断消息(以实现定义的方式标识),即使该行为也被明确指定为未定义或实现,定义。


23

C中,虽然aenum被视为单独的类型,但枚举器本身始终具有type int

C11-6.7.2.2枚举说明符

3枚举器列表中的标识符被声明为具有int类型的常量...

因此,您看到的行为是编译器扩展。

我要说的是,如果其枚举值太大,则仅扩展其中一个枚举数的大小是有意义的。


另一方面,在C ++中,所有枚举器都具有enum声明它们的类型。

因此,每个枚举器的大小必须相同。因此,整体大小enum被扩展以存储最大的枚举数。


11
它是一个编译器扩展,但生成诊断失败是不符合项。
Ben Voigt

16

正如其他人指出的那样,由于违反约束,代码格式错误(用C语言编写)。

GCC错误#71613(2016年6月报告)指出,一些有用的警告已被宏忽略。

使用系统标头中的宏时,有用的警告似乎被忽略了。例如,在下面的示例中,警告对于两个枚举都是有用的,但是仅显示一个警告。其他警告也可能发生相同的情况。

当前的解决方法可能是在宏前面加上一元运算+符:

enum en_e {
   en_e_foo,
   en_e_bar = +UINT64_MAX,
};

这在使用GCC 4.9.2的计算机上产生编译错误:

$ gcc -std=c11 -pedantic-errors -Wall main.c 
main.c: In function ‘main’:
main.c:9:20: error: ISO C restricts enumerator values to range ofint’ [-Wpedantic]
         en_e_bar = +UINT64_MAX

12

C11-6.7.2.2/2

定义枚举常量值的表达式应为整数常量表达式,其值可表示为int

en_e_bar=UINT64_MAX是违反约束条件的,因此上述代码无效。应通过确认C11草案中所述的实施来产生诊断消息:

如果预处理翻译单元或翻译单元包含任何语法规则或约束的违反,则合格的实现应产生至少一个诊断消息(以实现定义的方式标识)。[...]

似乎GCC有一些错误,未能产生诊断消息。(错误指出在回答格热戈日Szpetkowski


8
“未定义的行为”是运行时效果。 sizeof是一个编译时运算符。这里没有UB,即使有,它也不会影响sizeof
Ben Voigt

2
您应该找到不适合int的枚举的标准报价是UB。我对该声明表示高度怀疑,在此问题得到解决之前,我的投票将保持稳定的-1。
zneak

3
@Sergey:C标准实际上确实说过:“定义枚举常量值的表达式应该是一个整数常量表达式,其值可以表示为int。” 但违反此规定将是违反约束,需要诊断而不是UB。
Ben Voigt

3
@haccks:是吗?这是一个违反约束的行为,并且“如果预处理翻译单元或翻译单元包含违反任何语法规则或约束的条件,则符合规范的实现将产生至少一条诊断消息(以实现定义的方式标识),即使该行为也明确指定为未定义或实现定义。”
Ben Voigt

2
溢出和截断之间有区别。溢出是指您进行的算术运算产生的值对于预期结果类型而言过大,而有符号溢出为UB。截断是指您的值太大而无法以目标类型开头(例如short s = 0xdeadbeef),并且行为是实现定义的。
zneak

5

我看了一下标准,由于6.7.2.2p2,我的程序在C中似乎违反了约束:

约束:定义枚举常量值的表达式应为整数常量表达式,其值可表示为int。

并由于7.2.5在C ++中定义:

如果基础类型不是固定的,则每个枚举器的类型都是其初始化值的类型:—如果为枚举器指定了初始化器,则初始化值的类型与表达式相同,并且constant-expression必须是整数常数表达式(5.19)。—如果没有为第一个枚举数指定初始化器,则初始化值具有未指定的整数类型。—否则,初始化值的类型与前一个枚举器的初始化值的类型相同,除非递增的值不能用该类型表示,在这种情况下,该类型是未指定的整数类型,足以包含递增的值。如果不存在这样的类型,则程序格式错误。


3
它不是C语言中的“未定义”,而是“格式错误”,因为违反了约束。编译器必须生成有关违规的诊断。
Ben Voigt

@BenVoigt感谢您教我区别。在答案中进行了修复(之所以这样做,是因为我错过了其他答案中C ++标准的引号)。
PSkocik
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.