- 一般在大括号内声明的名称(name,或者严谨点说标识符)的可见范围是限制在大括号内的。然而C++98中的
enum
不同:它内部定义的名称会泄露到定义enum
自身的区域中。因此也得名 unscoped enum:
enum Color { black, white, red };
auto white = false; // VS报错: main::white重定义
- C++11版本的 scoped enums 通过
enum class
定义,不会导致名称的泄露:
enum class Color { black, white, red };
Color c = white; // error, 找不到white
Color c = Color::white; // ok
auto c = Color::white; // ok
- scoped enums 的第二个好处:枚举值为强类型,不会被意外地隐式转为整型(甚至接着被转为浮点型)。如果真的想进行转换,用最正确的方法,调用
cast
:
enum class Color { black, white, red };
Color c = red; // error
Color c = Color::red; // ok
if (c < 14.5) { // 对于enum,隐式转化为double;对于enum class,编译失败
auto factors = primeFactors(c); // 对于enum,隐式转化为int;对于enum class,编译失败
}
// 通过static_cast进行显式类型转换,编译通过
if (static_cast<double>(c) < 14.5) {
auto factors = primeFactors(static_cast<int>(c));
}
- 还有观点认为 scoped enum 的第三个好处是支持前向声明(forward declaration) 而 unscoped enum 不支持,这在C++11中是错误的。
- 首先说为什么C++98中 unscoped enum 不支持前向声明,是因为编译器为了内存使用效率,希望找一个能够用于该枚举类型且占用空间尽可能小的底层类型(underlying type)。比如上例中
Color
只有三个枚举值,那么编译器可能就会选择char
,而不是int
甚至long
。但是枚举内部每个名称的值是可以自定义的,例如:
enum Status {
good = 0,
failed = 1,
incomplete = 100,
corrupt = 200,
indeterminate = 0xFFFFFFFF
};
-
如果看不到枚举的完整定义,编译器就无法决定应该使用什么底层类型,因此C++98只允许枚举定义(definition)而不允许枚举声明(declaration)。但这样的缺点是,例如以上的
State
可能是一个庞大系统很多部分都依赖的类,一旦我们因为某一小部分需要对其做一点修改(例如增加一个状态),都导致整个系统不得不重新编译!这是前向声明能完美解决的问题。 -
C++11的方案是:给 scoped enum 一个默认的底层类型
int
,或者你可以在声明时指出要用的类型以进行覆盖。后者同样适用于 unscoped enum。
enum class Status; // scoped enum前向声明,底层类型为int
enum class Status : std::uint32_t; // scoped enum前向声明,底层类型为std::uint32_t
enum Color : std::uint8_t; // unscoped enum前向声明,底层类型为std::uint8_t
// scoped enum指定类型 + 定义
enum class Status : std::uint32_t {
good = 0,
failed = 1,
corrupt = 100
};
-
注:这里笔者这里发现 unscoped enum 不加底层类型也能在VS中通过编译,从测试结果来看,它也采用int作为默认类型。然而在GCC中相同声明会报编译错误。
-
尽管 scoped enum 有着以上优势,作者也提到 unscoped enum 在某些场合非常有用,例如从C++11的
std::tuple
中取元素,利用后者就会使语法简洁清晰:
using UserInfo = std::tuple<std::string, // 用户名
std::string, // 用户邮箱
std::size_t>; // 声誉值
UserInfo uInfo{ "1", "2", 3 };
auto& val1 = std::get<1>(uInfo); // 你真的记得住哪个编号对应哪个成员?
enum UserInfoFieldsUnscoped { uiName, uiEmail, uiReputation }; // 定义辅助的unscoped enum
auto& val2 = std::get<uiEmail>(uInfo); // 利用unscoped enum的隐式转换(转换为size_t)
enum class UserInfoFieldsScoped { uiName, uiEmail, uiReputation }; // 如果换用scoped enum
auto& val3 = std::get<static_cast<std::size_t>(UserInfoFieldsScoped::uiEmail)>(uInfo); // are you serious??
- 如果确实要用下面的方法,可以写一个通用的辅助函数来将 scoped enum 自身类型转换为其底层持有的数据类型。方法是使用
std::underlying_type
,它可以提取枚举类的底层类型(属于type traits)。根据条款14和15的描述,我们还应该用constexpr
和noexcept
来声明它,结果如下:
template<typename E>
constexpr auto toUType(E enumerator) noexcept
{
return static_cast<std::underlying_type_t<E>>(enumerator);
}
auto& val3 = std::get<toUType(UserInfoFieldsScoped::uiEmail)>(uInfo); // 简洁了一些, 也许...
总结
- C++98风格的
enum
现在被称为 unscoped enum。 - scoped enum 的枚举值仅在其内部可见。只能通过
cast
将其显式转换为其它类型。 - 两种 enum 都支持对底层数据类型的指明。scoped enum 的默认类型为
int
,unscoped enum 没有默认类型。 - scoped enum 总是可以被前向声明,unscoped enum 只能在指明底层类型时被前向声明。(笔者测试结果为,unscoped enum 在MSVC中不指定底层类型也能前向声明,默认采用
int
作为底层类型;GCC中与该描述相符。)