无作用域枚举、枚举器和基础类型C++中的歧义
Unscoped Enumeration, Enumerator & Underlying Type Ambiguity in C++
我正在学习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枚举声明):
在枚举说明符的右大括号后面,每个枚举器都有其枚举的类型。。。如果基础类型不是固定的,则每个枚举器在右大括号之前的类型确定为如下:
如果为枚举器指定了初始值设定项,则常数表达式应为积分常数表达式(8.6)。如果表达式具有无范围的枚举类型,枚举器具有该枚举类型的基础类型,否则它具有相同的键入作为表达式。
如果没有为第一个指定初始值设定项枚举器,其类型是未指定的有符号整数类型。
否则,枚举器的类型与前面的枚举器,除非递增的值不可表示在该类型中,在这种情况下,该类型是未指定的整型足以包含递增的值。如果不存在这样的类型,这个程序格式不正确。
此外,第10.2p7节规定:
对于基础类型不固定的枚举type是一个整数类型,可以表示所有枚举器值在枚举中定义。如果没有积分类型可以表示所有枚举器值,枚举格式不正确。是的实现定义了使用哪种积分类型作为基础类型,但基础类型不应大于int除非枚举器的值不能放入int或unsigned内部
因此我有以下问题:
- 为什么枚举
UEn
的底层类型是unsigned long
,而0x7FFFFFFFFFFFFFFF
是类型为long int
的整数常数,因此EN_L
的类型也是long int
。这是编译器错误还是定义良好的行为 - 当标准说
each enumerator has the type of its enumeration
时,这难道不意味着枚举器和枚举器的积分类型也应该匹配吗?这两个不同的原因是什么
底层类型是实现定义的。它只需要能够表示每个枚举器,除非需要,否则不能大于int
。根据dcl.enum.7,没有对签名性的要求(除了基类型必须能够表示每个枚举器之外),正如您已经发现的那样。这限制了枚举器类型的反向传播,超出了您的预期。值得注意的是,它没有说明枚举的基类型必须是任何枚举器初始值设定项的类型。
比起有符号整数,Clang更喜欢无符号整数作为枚举基;重要的是,枚举的类型不必与任何特定枚举器的类型匹配:它只需要能够表示每个枚举器。这是相当正常的,在其他情况下也能很好地理解。例如,如果您有EN_1 = 1
,即使1是int
,枚举的基类型不是int
或unsigned 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范围内的值):
int
、unsigned int
、long int
、unsigned long int
、long 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
- 不带大括号的枚举形式
- 枚举环境变量的惯用C++14/C++17方法
- 类似枚举的计算常量
- 如何正确实现和访问运算符的各种自定义枚举器
- 错误:从"int"到枚举c++的转换无效
- C++中构造函数中的枚举
- 访问在 C++ 结构中声明的枚举变量
- 枚举类'classname'的多重定义
- 强枚举类型定义:Clang Bug 还是 C++11 标准不确定性?
- typedef 枚举和枚举类有什么区别?
- 为什么我的开关/机箱在使用枚举时默认?
- 标准::可选枚举的比较运算符
- C++两个源文件之间共享的枚举的静态实例
- 打印没有铸件的枚举可以在C++中吗?
- 枚举成员与静态 int 成员?
- C++:枚举:错误:应使用标识符而不是"}"
- 带有 c++ 的枚举(输入检查)
- 在 qml 中使用 Q_ENUM 和 Q_PROPERTY 作为枚举类
- 为什么 int 类型的枚举类值不能用作 int
- 在 C++ 中输出枚举类类型的向量元素