优先选择scoped enums而不是unscoped enums
作为一个基本规则,在一个大括号里面声明一个名字会将其可见性限制在大括号定义的范围内。但是对于以C++98风格enum声明的枚举器就不再适用了。这类枚举器里面的名字属于包含了enum声明的范围空间,那也就意味着在那个范围内不能再有其他与之相同的名字:
enum Color {black, white, red}; //black.while,red are in same scope as Color
auto white = false; //error!white already declared in this scope
这类枚举器里面的名字泄露到包含枚举器定义的空间范围内,这一事实也使得对于这类枚举器有一个官方名称:unscoped enums.它们在C++11中新的对应物,scoped enums,并不会将名字泄露出去:
enum class Color {black,white,red};//black,while,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)
因为scoped enum都是通过”enum class”来声明的,它们有时候也被叫做enum classes。
scoped enums带来的命名空间污染减少已经足以让我们优先选择它们而不是unscoped enums,但是scoped enums还有第二个巨大的优势:它们的枚举成员更加强类型。unscoped enums里面的枚举成员会隐性转换成整型(从这开始,还可以转换成浮点型)。所以像下面这样的语义歪曲也完全合法:
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 = // compute prime factors
primeFactors(c); // of a Color (!)
…
}
然而,在enum后面简单加一个class,就能把unscoped enum转换成scoped enum,那情形就完全不一样了。scoped enum里面的枚举成员不存在隐性转换成其他类型:
enum class 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) { // error!can't compare Color and double
auto factors = // error!can't pass Color to
primeFactors(c); // function expecting std::size_t
…
}
如果你真的想执行Color到其他类型的转换,那就按照你之前类型转换的做法去做–使用cast:
if (static_cast<double>(c) < 14.5) { // odd code, but
// it's valid
auto factors = // suspect, but
primeFactors(static_cast<std::size_t>(c)); // it compiles
…
}
似乎scoped enums对比unscoped enums还有第三个优势,因为scoped enus可以前置声明,也就是说,他们的名字可以被声明,而无须指定里面的枚举成员。
enum Color; //error!
enum class Color; //fine
这其实是一种误解。在C++11中,unscoped enums也可以前置声明,但是需要做一点额外的工作。这部分工作基于如下事实:C++中的每一个枚举成员都有一个编译器决定的潜在整型类型。对于像Color这样的unscoped enum:
enum Color {black,white, red};
编译器可能选择char作为其潜在类型,因为只有三个值要表达。然而,有些enums的值的范围就很大:
enum Status { good = 0,
failed = 1,
incomplete = 100,
corrupt = 200,
indeterminate = 0xFFFFFFFF
};
这里需要表达的值范围从0到0xFFFFFFFF。除了一些不常见的机器(那里的char至少32位),编译器将不得不选择一个比char表示范围大的整型类型来表示Status的值。
为了更有效率地利用内存,编译器经常想选择一个能够覆盖一个enum所有枚举成员范围的最小的潜在类型。在某些情况下,编译器给速度做优化,而不是大小,在这种情况下,它们可能不会选择最小的可允许的潜在类型,但是它们肯定想能够优化大小。为了让这成为可能,C++98只支持enum定义(在那所有的枚举成员都列出来了);enum声明是不允许的。这就使得编译器能够在每个enum被使用前就选择一个最佳的潜在类型。
但是这种不允许前置声明的enum也存在缺点。最著名的恐怕就是增加了编译依赖性。再次思考一下Status enum:
enum Status { good = 0,
failed = 1,
incomplete = 100,
corrupt = 200,
indeterminate = 0xFFFFFFFF
};
这是一种很可能整个系统都要用到的enum,所以在系统中所有用到的地方都包含了它的头文件。如果引入了一个新的状态值,
enum Status { good = 0,
failed = 1,
incomplete = 100,
corrupt = 200,
audited = 500,
indeterminate = 0xFFFFFFFF
};
很可能整个系统都需要重新编译一遍,即使只有一个子系统–可能只有一个函数–用到了新的枚举成员。这是一种人们憎恶的事情,这也是一种C++11中前置声明消除的事情。例如,下面是一个完全合法的scoped enum的声明以及一个接受它作为参数的函数:
enum class Status; //forward declaration
void continueProcessing(Status s); //use of fwd-declaration enum
如果Status的定义被修改了,包含这些声明的头文件就不需要重新编译。更进一步,如果Status被修改了(比如,增加了audited枚举成员),但是continueProcessing的行为不受影响(比如,因为continueProcessing不使用audited),那么continueProcessing的实现也就不需要被重新编译了。
但是如果编译器在使用enum之前需要知道它的大小,那么既然C++98的enum做不到前置声明,C++11中的enum是如何做到前置声明的呢?答案很简单:scoped enum的潜在类型总是知道的,对于unscoped enum,你可以指定它。
对于scoped enums来说,默认的潜在类型是int:
enum class Status; //underlying type is int
如果默认的不适合,你可以覆盖它:
enum class Status:std::uint32_t; //underlying type for Status is std::uint32_t(from cstdint)
不管哪一种方式,编译器总是知道scoped enum里的枚举成员的大小。
为了指定unscoped enum的潜在类型,你需要做的和scoped enum一样,并且结果可以被前置声明:
enum Color: std::uint8_t; // fwd decl for unscoped enum;
// underlying type is
// std::uint8_t
潜在类型也可以在enum的定义中指定:
enum class Status: std::uint32_t { good = 0,
failed = 1,
incomplete = 100,
corrupt = 200,
audited = 500,
indeterminate = 0xFFFFFFFF
};
基于scoped enums可以防止命名空间污染以及对没有意义的隐性转换免疫等事实,当你听到还有至少一种情况下unscoped enum可能更有用时也许会大吃一惊。这种情况和C++11的std::tuple有关。例如,假设我们有一个tuple,包含有name,email和reputation的值:
using Userinfo = // type alias;See Item 9
std::tuple<std::string, //name
std::string, //email
std::size_t>; //reputation
尽管评论指出了tuple每一个部分(field)代表的含义,但是当你在一个其他源文件中碰到下面这样的代码,这恐怕帮不上什么忙:
UserInfo uInfo; //object of tuple type
...
auto val = std::get<1>(uInfo); //get value of field 1
作为一个程序员,你有一大堆东西需要留意。你真的应该被期待记住对应于用户邮箱地址的那个部分?我认不这么认为。使用一个unscoped enum来关联名字和顺序就能避免这种需求:
enum UserInfoFields {uiName, uiEmail, uiReputation};
UserInfo uinfo; //as before
...
auto val = std::get<uiEmail>(uInfo); //ah,get value of email field
让这个工作的是UserInfoField到std::size_t的隐性转换,而std::size_t则是std::get所需要的类型。
而使用scoped enum相对应的代码就更加冗长了:
enum class UserInfoFields {uiName, uiEmail, uiReputation};
UserInfo uInfo; //as before
...
auto val =
std::get<static_cast<std::size_t>(UserInfoFields::uiEmail)>(uInfo);
这种冗长可以通过写一个函数来减少,该函数接受一个枚举器并且返回相对应的std::size_t类型的值,但是需要一点技巧。std::get是一个模板,你提供的值是一个模板参数(注意尖括号的使用,不是小括号),所以将枚举器转换std::size_t的函数必须在编译期间就得出结果。正如Item 15所说的,这意味着它一定是一个constexpr函数。
实际上,它应该是一个constexpr函数模板,因为它应该能处理各种类型的enum。如果我们将要做这种泛化,我们应该对返回类型也做泛化。我们应该返回enum的潜在类型,而不是返回std::size_t。这个可以通过std::underlying_type型别特性来实现。(参看Item 9获取更多关于型别特性的信息)最后,我们将它声明成noexcept(见Item 14),因为我们知道它永远不会产生异常。结果就是一个接受任意枚举类型并且返回其置的函数模板toUType作为一个编译期常量:
template<typename E>
constexpr typename std::underlying_type<E>::type
toUType(E enumerator) noexcept
{
return
static_cast<typename
std::underlying_type<E>::type>(enumerator);
}
在C++14中,toUType可以通过将typename std::underlying_type<T>::type替换成std::underlying_type_t(见Item 9)来简化:
template<typename E> // C++14
constexpr std::underlying_type_t<E>
toUType(E enumerator) noexcept
{
return static_cast<std::underlying_type_t<E>>(enumerator);
}
C++14中更简洁的auto返回类型也是合法的:
template<typename E> // C++14
constexpr auto
toUType(E enumerator) noexcept
{
return static_cast<std::underlying_type_t<E>>(enumerator);
}
不管它怎么写,toUType允许我们像这样访问tuple的一个部分:
auto val = std::get<toUType(UserInfoFields::uiEmail)>(uInfo);
这样还是比使用unscoped enum要写的多,但是它避免了命名空间污染以及涉及到枚举器的不经意的转换。在许多情况下,你可能会认为多敲一些字符是为了避免陷入enum技术带来的陷进中所付出的合理代价。
要点记忆
- C++98风格的enums现在称为unscoped enums
- scoped enums的枚举成员只在enum内部可见,他们得使用cast才能转换成其他类型
- scoped和unscoped enum都支持对潜在类型进行指定。scoped enum的默认潜在类型是int,unscoped enum没有默认潜在类型
- scoped enums总是可以前置声明。unscoped enums只有在指定了潜在类型之后才可以前置声明