C++ 中的无作用域枚举、枚举器和底层类型歧义

Che*_*har 5 c++ enums gcc c++11 c++17

我正在浏览 C++ 标准 n4713.pdf。考虑下面的代码:

#include <iostream>
#include <type_traits>

enum UEn
{
    EN_0,
    EN_1,
    EN_L = 0x7FFFFFFFFFFFFFFF            // EN_L has type "long int"
};                                       // UEn has underlying type "unsigned long int"

int main()
{
    long lng = 0x7FFFFFFFFFFFFFFF;

    std::cout << std::boolalpha;
    std::cout << "typeof(unsigned long == UEn):" << std::is_same<unsigned long, std::underlying_type<UEn>::type>::value << std::endl;  // Outputs "true"
    std::cout << "sizeof(EN_L):" << sizeof(EN_L) << std::endl;
    std::cout << "sizeof(unsigned):" << sizeof(unsigned) << std::endl;
    std::cout << "sizeof(unsigned long):" << sizeof(unsigned long) << std::endl;
    std::cout << "sizeof(unsigned long):" << sizeof(unsigned long long) << std::endl;

    lng = EN_L + 1;                      // Invokes UB as EN_L is 0x7FFFFFFFFFFFFFFF and has type "long int"

    return 0;
}
Run Code Online (Sandbox Code Playgroud)

上面的代码输出(在 g++-8.1、Clang 上测试):

typeof(unsigned long == UEn):true sizeof(EN_L):8 sizeof(unsigned):4 sizeof(unsigned long):8 sizeof(unsigned long):8

根据第 10.2p5 节(10.2 枚举声明):

在枚举说明符的右大括号之后,每个枚举器都有其枚举的类型……如果基础类型不固定,则在右大括号之前的每个枚举器的类型确定如下:

  • 如果为枚举器指定了初始值设定项,则常量表达式应为整型常量表达式 (8.6)。如果表达式具有无作用域枚举类型,则枚举器具有该枚举类型的基础类型,否则它具有与表达式相同的类型。

  • 如果没有为第一个枚举器指定初始值设定项,则其类型是未指定的有符号整数类型。

  • 否则,枚举器的类型与前面的枚举器的类型相同,除非递增的值在该类型中无法表示,在这种情况下,该类型是足以包含递增值的未指定整数类型。如果不存在这样的类型,则程序格式错误。

此外,第 10.2p7 节指出:

对于底层类型不固定的枚举,底层类型是一个整型,可以表示枚举中定义的所有枚举器值。如果没有整数类型可以表示所有枚举器值,则枚举格式错误。使用哪种整数类型作为底层类型是实现定义的,除非底层类型不应大于 int,除非枚举数的值不能放入 int 或 unsigned int 中。


因此,我有以下问题:

  1. 为什么 enum 的基础类型UEnan unsigned longwhen 0x7FFFFFFFFFFFFFFF是 type 的整数常量,long int因此 type ofEN_L也是long int. 这是编译器错误还是定义明确的行为?
  2. 当标准说 时each enumerator has the type of its enumeration,难道不应该暗示枚举器和枚举的整数类型也应该匹配吗?这两个彼此不同的原因可能是什么?

zne*_*eak 2

底层类型是实现定义的。它只需能够代表每个枚举器,并且除非int需要,否则它不能大于。正如您已经发现的,根据dcl.enum.7 ,没有对符号的要求(除了基本类型必须能够表示每个枚举器)。这对枚举器类型的反向传播的限制比您想象的要多。值得注意的是,它没有在任何地方说枚举的基类型必须是任何枚举器的初始值设定项的类型。

与有符号整数相比,Clang 更喜欢使用无符号整数作为枚举基数;这里的所有都是它的。重要的是,枚举的类型不必与任何特定枚举器的类型匹配:它只需能够表示每个枚举器。这在其他情况下是相当正常且容易理解的。例如,如果您有,那么即使 1 是,枚举的基本类型不是或,EN_1 = 1您也不会感到惊讶。intunsigned intint

您说的类型也是正确的0x7fffffffffffffffis long。Clang 同意你的观点,但是它隐式地将常量转换为unsigned long

TranslationUnitDecl
`-EnumDecl <line:1:1, line:5:1> line:1:6 Foo
  |-EnumConstantDecl <line:2:5> col:5 Frob 'Foo'
  |-EnumConstantDecl <line:3:5> col:5 Bar 'Foo'
  `-EnumConstantDecl <line:4:5, col:11> col:5 Baz 'Foo'
    `-ImplicitCastExpr <col:11> 'unsigned long' <IntegralCast>
      `-IntegerLiteral <col:11> 'long' 576460752303423487
Run Code Online (Sandbox Code Playgroud)

这是允许的,因为正如我们之前所说,枚举的基类型不需要是任何枚举器的逐字类型。

当标准说每个枚举器都有枚举的类型时,这意味着 的类型EN_1位于enum UEn枚举的右大括号之后。请注意提到的“右大括号之后”和“右大括号之前”。

在右大括号之前,如果枚举没有固定类型,则每个枚举数的类型是其初始化表达式类型的类型,但这只是临时的。例如,这允许您在EN_2 = EN_1 + 1不进行强制转换EN_1的情况下进行编写,即使是在enum class. 在右大括号之后,情况不再如此。您可以通过检查错误消息或查看反汇编来欺骗编译器向您显示:

template<typename T>
T tell_me(const T&& value);

enum Foo {
    Baz = 0x7ffffffffffffff,
    Frob = tell_me(Baz)
    // non-constexpr function 'tell_me<long>' cannot be used in a constant expression
};
Run Code Online (Sandbox Code Playgroud)

请注意,在本例中T被推断为long,但在右大括号之后,它被推断为Foo

template<typename T>
T tell_me(const T&& value);

enum Foo {
    Baz = 0x7ffffffffffffff
};

int main() {
    tell_me(Baz);
    // call    Foo tell_me<Foo>(Foo const&&)
}
Run Code Online (Sandbox Code Playgroud)

如果您希望使用 Clang 对枚举类型进行签名,则需要使用: base_type语法指定它,或者需要有一个负枚举器。