10 Prefer scoped enums to unscoped enums
一个一般规则是大括号里面声明的变量的可见性会被限制在大括号范围内。但是 C++11 之前的枚举 enum
是一个例外,它的范围是所有包含这个枚举的地方。比如
enum Color
{
black,
white,
red
}; // black, white, red are in same scope as Color
auto white = false; // error! white already declared in this scope
官方称呼是未限定范围的(unscoped
)的枚举,与此相对,C++11 引入了限定范围的枚举(scoped enum
)。
enum class Color
{
black,
white,
red
}; // black, white, red are scoped to Color
auto white = false; // fine, no other "white" in scope
Color c = white; // error! no enumerator named "white" is in this scope
Color c = Color::white; // fine
auto c = Color::white; // also fine (and in accord with Item 5's advice)
由于关键字是 enum class
所有有时也称为枚举类。
除了限定范围外,枚举类还有一个好处:更强的类型。未限定范围的枚举能够隐式地转化成整数(进而可以转化为浮点数)。所以下面代码的语义看似荒唐,但是是合法的。
enum Color
{
black,
white,
red
}; // unscoped enum
std::vector<std::size_t> // func. returning
primeFactors(std::size_t x); // prime factors of x
Color c = red;
if (c < 14.5) // compare Color to double (!)
{
auto factors = primeFactors(c); // compute prime factors of a Color (!)
}
只要将 enum
后添加一个 class
关键字,一切都变了。
enum class Color
{
black,
white,
red
}; // enum is now scoped
Color c = Color::red; // as before, but with scope qualifier
if (c < 14.5) // error! can't compare Color and double
{
// error! can't pass Color to function expecting std::size_t
auto factors = primeFactors(c);
}
如果真的想转化成其他类型,需要显式地实现。
if (static_cast<double>(c) < 14.5) // odd code, but it's valid
{
// suspect, but it compiles
auto factors = primeFactors(static_cast<std::size_t>(c));
}
第三个好处是限定范围的枚举可以前置声明。
其实非限定范围的枚举也可以前置声明,不过需要一些额外的工作。利用的是对于 C++ 中的枚举类型,编译器会确定使用某个长度的整数作为其底层类型(underlying type
)。比如
编译器可能会使用 char
类型,因为只有三种值。不过有的枚举的值域范围会比较大,比如
值域范围从 0
到 0xFFFFFFFF
,那么编译器使用比 char
长的整数类型。
编译器会尽可能选择一个短的整数类型,有的时候会兼顾性能,不管怎么,编译器希望能够优化大小。为了能够优化,规定枚举只能定义不能声明。
不能前向声明有很多缺点。一个很大的问题是编译依赖问题。比如有如下枚举
这个枚举大概率系统的很多地方都在使用,所以都依赖这个头文件。我们需要加一个新的状态
enum Status
{
good = 0,
failed = 1,
incomplete = 100,
corrupt = 200,
audited = 500,
indeterminate = 0xFFFFFFFF
};
那么近乎整个系统都需要重新编译。如果使用前向声明
enum class Status; // forward declaration
void continueProcessing(Status s); // use of fwd-declared enum
即使 Status
修改了,包含这些声明的头文件也不需要重新编译。如果 continueProcessing
的实现没有变,那么也不需要重新编译。
既然现需要知道长度,C++11 是如何实现前向声明的呢?很简单,对于限定范围的枚举,总是有长类型信息即可。对于非限定范围的枚举,必须指定一个类型信息。
enum class Status; // underlying type is int
enum class Status : std::uint32_t; // underlying type for Status is
// std::uint32_t (from <cstdint>)
不管是上述那种情况,编译器都知道底层类型。
给未限定范围的枚举指定底层类型,可以在前向声明的时候使用相同的语法指定,也可以在定义的时候指定。
enum Color : std::uint8_t; // fwd decl for unscoped enum;
// underlying type is std::uint8_t
enum class Status : std::uint32_t
{
good = 0,
failed = 1,
incomplete = 100,
corrupt = 200,
audited = 500,
indeterminate = 0xFFFFFFFF
};
不过,当和 std::tuple
配合的时候,未限定范围的枚举很有用。比如下面的例子,使用一个 tuple
保存用户的名字、邮箱地址和声望值。
using UserInfo = // type alias; see Item 9
std::tuple<std::string, // name
std::string, // email
std::size_t>; // reputation
虽然有各个字段的注释,但是如果在其他地方看到下面的代码,还是很难理解。
使用 1 表示用户的邮箱地址,实在是不靠谱。这时声明一个未限定范围的枚举就很有用了。
enum UserInfoFields
{
uiName,
uiEmail,
uiReputation
};
UserInfo uInfo; // object of tuple type
auto val = std::get<uiEmail>(uInfo); // ah, get value of email field
这里 UserInfoFields
隐式地转化成了 std::size_t
。
如果使用限定范围的枚举,就会麻烦很多。
enum class UserInfoFields
{
uiName,
uiEmail,
uiReputation
};
UserInfo uInfo; // as before
auto val = std::get<static_cast<std::size_t>(UserInfoFields::uiEmail)>(uInfo);
可以写一个接受 UserInfoFields
返回 std::size_t
的函数来简化这个写法。不过由于 std::get
是一个模板函数,那么需要在编译期确定这个值,那么这里需要一个 constexpr
修饰的函数,参考 Item 15。
实际上,我们需要的是 constexpr
模板函数,接受任意枚举,然后返回其底层类型,也就是一般化上述函数的输入和输出。std::underlying_type
能够帮助我们得到枚举的底层类型。下面的 toUType
函数接受任意枚举类型,在编译期返回它的值。
template <typename E> // C++14
constexpr auto
toUType(E enumerator) noexcept
{
return static_cast<std::underlying_type_t<E>>(enumerator);
}
这里使用了 Item 9 提到的 std::underlying_type_t
和 Item 3 提到的使用 auto
表示返回类型。
下面是简写的客户端代码。
这仍旧比使用未限定范围的枚举略微复杂一些。不过限定范围的枚举有它的好处,所以很多时候,多写几个字符也是可以接受的,以此来规避长期存在的未限定范围的枚举的陷阱。
Things to Remember
- C++98-style
enums
are now known as unscopedenums
. - Enumerators of scoped
enums
are visible only within theenum
. They convert to other types only with a cast. - Both scoped and unscoped
enums
support specification of the underlying type. The default underlying type for scopedenums
isint
. Unscopedenums
have no default underlying type. - Scoped
enums
may always be forward-declared. Unscopedenums
may be forward-declared only if their declaration specifies an underlying type.