C ++中的无范围枚举,枚举器和底层类型歧义

时间:2019-01-21 18:32:21

标签: c++ c++11 gcc enums 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;
}

上面的代码输出(在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枚举声明):

  

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

     
      
  • 如果为枚举器指定了初始化程序,则   constant-expression应该是整数常量表达式(8.6)。如果   表达式具有无范围的枚举类型,枚举器具有   该枚举类型的基础类型,否则具有相同的值   类型作为表达式。

  •   
  • 如果没有为第一个指定初始化程序   枚举数,其类型是未指定的带符号整数类型。

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

  •   

此外,第10.2p7节规定:

  

对于基础类型不固定的枚举,基础   type是一个整数类型,可以表示所有枚举器值   在枚举中定义。如果没有整数类型可以代表所有   枚举值,则枚举格式不正确。它是   实现定义哪种整数类型用作基础   类型,但基础类型不得大于int   除非枚举数的值不能适合int或unsigned   


因此,我有以下问题:

  1. UEn是类型unsigned long的整数常量,因此0x7FFFFFFFFFFFFFFF的类型也是{时,为什么枚举long int的基础类型是EN_L {1}}。这是编译器错误还是行为明确?
  2. 当标准说 long int 时,是否不应该暗示枚举数和枚举的整数类型也应该匹配?这两个彼此不同的原因可能是什么?

3 个答案:

答案 0 :(得分:2)

基础类型是实现定义的。它仅必须能够表示每个枚举数,并且除非需要,否则不能大于int。正如您已经发现的那样,对于每个dcl.enum.7,没有对签名的要求(除了基本类型必须能够代表每个枚举数)。这限制了枚举器类型的反向传播,超出了您的假设。值得注意的是,它并没有说枚举的基本类型必须是任何枚举器的初始化程序的类型。

Clang首选无符号整数作为枚举基,而不是有符号整数;这里的所有都是它的。重要的是,枚举的类型不必匹配任何特定的枚举器的类型:它仅必须能够表示每个枚举器。这在其他情况下是相当正常且容易理解的。例如,如果您有EN_1 = 1,即使1是int,枚举的基类型也不是unsigned intint也不会令您感到惊讶。 / p>

您也可以正确地说0x7fffffffffffffff的类型为long。 Clang同意您的看法,但是implicitly casts the constant to 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

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

当标准规定每个枚举器都有枚举的类型时,这意味着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
};

请注意,在这种情况下,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&&)
}

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

答案 1 :(得分:2)

我认为,这种(绝对不直观的)警告的答案在于7.6整体促销[conv.prom]:

  

未范围枚举类型的prvalue,其基础类型不是   固定(10.2)可以转换为以下第一个的prvalue   以下类型可以代表枚举的所有值   (即10.2中所述的b min 到b max 范围内的值):   intunsigned intlong intunsigned long intlong long int或   unsigned long long int

也就是说,如果您的基础类型不是固定的,并且您在表达式中使用了枚举成员,则不一定将其转换为枚举的基础类型。相反,它将转换为该列表中所有成员都适合的第一种类型。

别问我为什么,这条规则对我来说似乎很疯狂。

本节继续说:

  

基础类型为的无范围枚举类型的prvalue   固定(10.2)可以转换为其基础类型的prvalue。

即如果您使用unsigned long修复基础类型:

enum UEn : unsigned long
...

然后警告消失了。

摆脱警告(并使基础类型不固定)的另一种方法是添加需要存储unsigned long的成员:

EN_2 = 0x8000000000000000

然后,警告消失。

好问题。我从回答中学到了很多。

答案 2 :(得分:0)

第10.2p5节的措词明确指出“ ...在右括号之前”建议以下解释。在枚举类型定义中的枚举类型(在大括号之前)被选择为某种足以表示其值的整数类型。然后,可以在同一枚举的后续枚举数定义中重用此值。当遇到枚举类型的关闭括号时,编译器将选择一个足够大的整数类型来表示所有枚举器值。定义枚举类型后,所有枚举器值都具有相同的类型(即枚举类型),并共享枚举的基础类型。例如:

#include <iostream>
#include <typeinfo>
#include <type_traits>

enum E1
{
  e1 = 0, // type of the initializer (int), value = 0
  e2 = e1 + 1U, // type of the initializer (unsigned = int + unsigned), value = 1U
  e3 = e1 - 1, // type of the initializer (int = int - int), value = -1
}; // range of values [-1, 1], underlying type is int

int main()
{
   std::cout << typeid(std::underlying_type<E1>::type).name() << '\n';
   std::cout << typeid(e1).name() << '\n';
   std::cout << typeid(e2).name() << '\n';
   std::cout << typeid(e3).name() << '\n';
}

使用clan5和gcc8运行,并输出:

i
2E1
2E1
2E1