Item 10 Item10:优先考虑限域枚举而非未限域枚举
条款10:限域枚举和非限域枚举
通常来说,在花括号中声明⼀个名字会限制它的作⽤域在花括号之内。但这对于 C++98 ⻛格的 enum
中声明的枚举名是不成⽴的。这些在 enum
作⽤域中声明的枚举名所在的作⽤域也包括 enum
本⾝,也就是说这些枚举名和 enum
所在的作⽤域中声明的相同名字没有什么不同
1 | enum Color { black, white, red }; // black, white, red 和 |
事实上这些枚举名泄漏进和它们所被定义的 enum
域⼀样的作⽤域。有⼀个官⽅的术语:未限域枚举(unscoped enum)在 C++11 中它们有⼀个相似物,限域枚举(scoped enum),它不会导致枚举名泄漏:
1 | enum class Color { black, white, red };// black, white, red被限制在Color域内 |
因为限域枚举是通过 enum class
声明,所以它们有时候也被称为**枚举类(enum classes)**。
使⽤限域枚举减少命名空间污染是⼀个⾜够合理使⽤它的理由,其实限域枚举还有第⼆个吸引⼈的优点:在它的作⽤域中,枚举名是强类型的。未限域枚举中的枚举名会隐式转换为整型(现在,也可以转换为浮点类型)。因此下⾯这种歪曲语义的做法也是完全有效的:
1 | enum Color { black, white, red }; // 未限域枚举 |
在 enum
后⾯写⼀个 class
就可以将⾮限域枚举转换为限域枚举,接下来就是完全不同的故事展开了。现在不存在任何隐式转换可以将限域枚举中的枚举名转化为任何其他类型。
1 | enum class Color { black, white, red }; // Color现在是限域枚举 |
如果你真的很想执⾏ Color
到其他类型的转换,和平常⼀样,使⽤正确的类型转换运算符扭曲类型系统:
1 | if (static_cast<double>(c) < 14.5) { // 奇怪的代码,但是有效 |
似乎⽐起⾮限域枚举而⾔限域枚举有第三个好处,因为限域枚举可以前置声明。⽐如,它们可以不指定枚举名直接前向声明:
1 | enum Color; // 错误 |
其实这是⼀个误导。在 C++11 中,⾮限域枚举也可以被前置声明,但是只有在做⼀些其他⼯作后才能实现。这些⼯作来源于⼀个事实:在 C++ 中所有的枚举都有⼀个由编译器决定的整型的基础类型。对于⾮限域枚举⽐如 Color
,
1 | enum Color { black, white, red }; |
编译器可能选择 char
作为基础类型,因为这⾥只需要表⽰三个值。然而,有些枚举中的枚举值范围可能会⼤些,⽐如:
1 | enum Status{ |
这⾥值的范围从 0 到 0xFFFFFFFF。除了在不寻常的机器上(⽐如⼀个 char
⾄少有 32bits 的那种),编译器都会选择⼀个⽐ char
⼤的整型类型来表⽰ Status
。
为了⾼效使⽤内存,编译器通常在确保能包含所有枚举值的前提下为枚举选择⼀个最小的基础类型。在⼀些情况下,编译器将会优化速度,舍弃⼤小,这种情况下它可能不会选择最小的基础类型,而是选择对优化⼤小有帮助的类型。为此,C++98 只⽀持枚举定义(所有枚举名全部列出来);枚举声明是不被允许的。这使得编译器能为之前使⽤的每⼀个枚举选择⼀个基础类型。
但是不能前置声明枚举也是有缺点的。最⼤的缺点莫过于它可能增加编译依赖。再次考虑 Status
枚举:
1 | enum Status { good = 0, |
这种 enum
很有可能⽤于整个系统,因此系统中每个包含这个头⽂件的组件都会依赖它。如果引⼊⼀个新状态值,
1 | enum Status { good = 0, |
那么可能整个系统都得重新编译,即使只有⼀个⼦系统——或者⼀个函数使⽤了新添加的枚举名。这是⼤家都不希望看到的。C++11 中的前置声明可以解决这个问题。⽐如这⾥有⼀个完全有效的限域枚举声明和⼀个以该限域枚举作为形参的函数声明:
1 | enum class Status; // forward declaration |
即使 Status
的定义发⽣改变,包含这些声明的头⽂件也不会重新编译。而且如果 Status
添加⼀个枚举名(⽐如添加⼀个 audited
),continueProcessing
的⾏为不受影响(因为 continueProcessing
没有使⽤这个新添加的 audited
),continueProcessing
也不需要重新编译。但是如果编译器在使⽤它之前需要知晓该枚举的⼤小,该怎么声明才能让 C++11 做到 C++98 不能做到的事情呢?
限域枚举的基础类型总是已知的,而对于⾮限域枚举,你可以指定它。默认情况下,限域
举的基础类型是 int
:
1 | enum class Status; // 基础类型是int |
如果默认的 int
不适⽤,你可以重写它:
1 | enum class Status: std::uint32_t; // Status的基础类型 |
不管怎样,编译器都知道限域枚举中的枚举名占⽤多少字节。要为⾮限域枚举指定基础类型,你可以同上,然后前向声明⼀下:
1 | enum Color: std::uint8_t; // 为⾮限域枚举Color指定 |
基础类型说明也可以放到枚举定义处:
1 | enum class Status: std::uint32_t { good = 0, |
限域枚举避免命名空间污染而且不接受隐式类型转换,但是有⼀种情况下⾮限域枚举是很有⽤的。那就是获取 C++11 tuples 中的字段的时候。⽐如在社交⽹站中,假设我们有⼀个 tuple
保存了⽤⼾的名字,email
地址,声望点:
1 | using UserInfo = std::tuple<std::string, std::string, std::size_t> ; // 名字 email地址 声望 |
虽然注释说明了 tuple
各个字段对应的意思,但当你在另⽂件遇到下⾯的代码那之前的注释就不是那么有⽤了:
1 | UserInfo uInfo; // tuple对象 |
可以使⽤⾮限域枚举将名字和字段编号关联起来以避免上述需求:
1 | enum UserInfoFields { uiName, uiEmail, uiReputation }; |
之所以它能正常⼯作是因为 UserInfoFields
中的枚举名隐式转换成 std::size_t
了,其中 std::size_t
是 std::get
模板实参所需的。
对应的限域枚举版本就很啰嗦了:
1 | enum class UserInfoFields { uiName, uiEmail, uiReputation }; |
总结
- C++98的枚举即⾮限域枚举
- 限域枚举的枚举名仅在enum内可⻅。要转换为其它类型只能使⽤cast。
- ⾮限域/限域枚举都⽀持基础类型说明语法,限域枚举基础类型默认是 int 。⾮限域枚举没有默认基础类型。
- 限域枚举总是可以前置声明。⾮限域枚举仅当指定它们的基础类型时才能前置。