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

Unscoped Enumeration, Enumerator & Underlying Type Ambiguity in C++

本文关键字:枚举 C++ 类型 歧义 作用域      更新时间:2023-10-16

我正在学习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. 为什么枚举UEn的底层类型是unsigned long,而0x7FFFFFFFFFFFFFFF是类型为long int的整数常数,因此EN_L的类型也是long int。这是编译器错误还是定义良好的行为
  2. 当标准说each enumerator has the type of its enumeration时,这难道不意味着枚举器和枚举器的积分类型也应该匹配吗?这两个不同的原因是什么

底层类型是实现定义的。它只需要能够表示每个枚举器,除非需要,否则不能大于int。根据dcl.enum.7,没有对签名性的要求(除了基类型必须能够表示每个枚举器之外),正如您已经发现的那样。这限制了枚举器类型的反向传播,超出了您的预期。值得注意的是,它没有说明枚举的基类型必须是任何枚举器初始值设定项的类型。

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

你说0x7fffffffffffffff的类型是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

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

当标准规定每个枚举器都有枚举的类型时,这意味着EN_1的类型是枚举的大括号后的enum UEn。请注意"在大括号后面"answers"在大大括号之前"提到的内容。

在使用大括号之前,如果枚举没有固定类型,则每个枚举器的类型都是其初始化表达式类型的类型,但这只是暂时的。例如,这允许您在不强制转换EN_1的情况下编写EN_2 = EN_1 + 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语法指定它,或者需要使用负枚举器。

我相信这个(公认的非直观的)警告的答案是7.6 Integral promotions[conf.prom]:

基础类型不是的无范围枚举类型的prvaluefixed(10.2)可以转换为以下类型可以表示枚举的所有值(即,如10.2所述,在bmin到bmax范围内的值):intunsigned intlong intunsigned long intlong long int或CCD_ 35。

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

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

本节接着说:

未分级枚举类型的prvalue,其基础类型为fixed(10.2)可以转换为其基础类型的prvalue。

例如,如果使用unsigned long:修复底层类型

enum UEn : unsigned long
...

那么警告就消失了。

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

EN_2 = 0x8000000000000000

然后,警告再次消失。

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

第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