【问题标题】:Unscoped Enumeration, Enumerator & Underlying Type Ambiguity in C++C++ 中的无范围枚举、枚举器和基础类型歧义
【发布时间】:2019-06-15 04:40:10
【问题描述】:

我正在阅读 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):truesizeof(EN_L):8sizeof(unsigned):4sizeof(unsigned long):8sizeof(unsigned long):8

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

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

  • 如果为枚举数指定了初始值设定项,则 常量表达式应该是一个整数常量表达式(8.6)。如果 表达式具有无范围的枚举类型,枚举数具有 该枚举类型的基础类型,否则它具有相同的 输入为表达式。

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

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

此外,第 10.2p7 节指出:

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


因此我有以下问题:

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

【问题讨论】:

  • 0x7FFFFFFFFFFFFFFF 是不确定符号的整数常数。您的编译器选择将其存储为 unsigned long
  • @JonHarper:标准规定:“整数文字的类型是表 7 中对应列表的第一个,其中可以表示其值:二进制、八进制或十六进制文字- int, unsigned int, long int, unsigned long int, long long int, unsigned long long int”。 编译器不能随心所欲,不是吗?
  • 符合标准的编译器可以在某些情况下(UB,未指定的行为)随心所欲地采取行动,但否则你是正确的。然而,编译器很少(如果有的话)真正符合标准。

标签: c++ c++11 gcc enums c++17


【解决方案1】:

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

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

您说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 语法指定它,或者您需要使用负枚举数。

【讨论】:

  • 负枚举值无论如何都可以很好地指示错误代码。每当我创建一个新的枚举时,我总是将error = -1 指定为第一个条目。稍后您可以比较if code &gt; SomeEnum::error。但是,就像您建议的那样,使用 : base_type 语法来避免有符号和无符号类型之间的算术逻辑错误是一个好主意。如果有一些有符号和一些无符号枚举,在没有定义负值的枚举的情况下,这将是令人困惑的。
  • @zneak 我同意,但是我试图提出的一点是,即使所有枚举数都可以放入 long,编译器也会显式地对枚举强加无符号类型。只要它不适合它们,就保持类型与枚举器的类型相同是否有意义。最重要的是,GCC 和 Clang 都有相同的行为。好吧,我想如果标准说它是实现定义的,那么重点是没有意义的!顺便说一句,模板的技巧 +1!
【解决方案2】:

我相信这个(诚然不直观)警告的答案在 7.6 Integral Promotions [conv.prom] 中:

无作用域枚举类型的纯右值,其基础类型不是 固定(10.2)可以转换为第一个的prvalue 以下类型可以表示枚举的所有值 (即,bmin 到 bmax 范围内的值,如 10.2 中所述): intunsigned intlong intunsigned long intlong long intunsigned long long int.

即,如果您的基础类型不固定,并且您在表达式中使用枚举成员,则它不一定转换为枚举的基础类型。相反,它会转换为该列表中所有成员都适合的第一个类型。

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

本节继续说:

无作用域枚举类型的纯右值,其基础类型为 fixed (10.2) 可以转换为其基础类型的纯右值。

即如果您使用unsigned long 修复底层类型:

enum UEn : unsigned long
...

然后警告消失。

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

EN_2 = 0x8000000000000000

然后,警告又消失了。

好问题。我在回答中学到了很多东西。

【讨论】:

  • 这意味着,枚举的基础类型在以下 2 种情况下是固定的 - 1. 作用域枚举 2. 使用“枚举基”限定符无作用域,(第 10.2 节p5): "每个枚举也有一个底层类型。底层类型可以使用 enum-base 显式指定。对于作用域枚举类型,如果没有显式指定,底层类型是 int。在这两个中在这种情况下,基础类型被称为是固定的”。因此,尽管枚举的基础类型是“无符号”,“7.6 Integral Promotions”导致增量为 UB!男孩,我只是不知道如何反应。真是一团糟!
【解决方案3】:

第 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

【讨论】:

    猜你喜欢
    • 2019-08-29
    • 1970-01-01
    • 2010-10-25
    • 1970-01-01
    • 2018-03-22
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多