先来一个通用规则:
如果在一对大括号里声明一个名字,则该名字的可见性救兵限定在括号括起来的作用域内。
但这个规则有个例外 C++98风格的枚举型别不受此限制。
这个例外导致一下代码会有错误:
enum Color {black, white, red}; //black, white, red所在的作用域和Color相同
auto white = false; //错误!white已经在范围内被声明过了
优势1:不会造成命名污染
这种枚举量名字泄露会带来很多弊端,有时候一不小心就发现名字被使用了,官方将这种枚举称之为不限范围的枚举型别
,有时也被称作枚举类
。而在C++11中,它的对等物,限定作用域枚举型别,将修复这种弊端。示例如下:
enum class Color {black, white, red}; //限定所在作用域的枚举型别
auto white = false; //没问题,white被限定在Color内
Color c = white; //错误,范围内没有white的枚举量
Color c = Color::white; //没问题!
auto c = Color::white; //同样没问题!
优势2:限定作用域的枚举量是强型别的
看以下代码:
enum Color {black, white, red}; //不限定范围的枚举型别
std::vector<std::size_t>
primeFactors(std::size_t x); //函数,返回x的质因数
Color c = red;
...
if (c < 14.5) { //将Color型别和double型别值比较!
auto factors = primeFactors(c); //计算一个Color型别的质因数
...
}
仅仅是把一个简单的class
加入到enum
之后,语义就完全不同了,限定作用域的枚举类型不存在任何隐式转换路径:
enum class Color {black, white, red}; //不限定作用域的枚举型别
Color c = Color::red; //同前,但要加范围限定饰词
if (c < 14.5){ //错误!不能将Color和double比较
auto factors = //错误!不支持隐式转换
primeFactors(c);
}
如果真的要比较,那么只用 static_cast<double>(c)
即可。
优势3:限定作用域的枚举型别可以前置声明
实例代码如下:
enum Color; //错误!
enum class Color; //没问题
其实这种限制并非因为枚举的原因不能前置声明,而是基于以下这个事实导致必须在前置声明中定义好:
一切枚举型别在C++里都会由编译器来选择一个整数型别作为其底层型别。
类似如下代码:
enum Color {black, white, red};
编译器会选择char
作为底层型别,因为只有3个值要表示。有些枚举型别取值范围就大得多,例如:
enum Status { good = 0,
failed = 1,
incomplete = 100,
corrupt = 200,
indeterminate = 0xFFFFFFFF};
以上代码需要表示的范围从 0
~ 0xFFFFFFFF
,那么对于这种类型就不是用char来存储了。一般来说编译器会选择满足范围的最小底层型别。但在某种情况下,编译器会用空间换时间,所以无限制枚举型别就没有一个固定的规则,要求必须在声明的时候,逐个确认底层型别选择哪一种。
不过以上前置声明能力的缺失还是会造成一些弊端。最大弊端是它会增加编译依赖性。考虑如下代码:
enum Status { good = 0,
failed = 1,
incomplete = 100,
corrupt = 200,
audited = 500, //由于新需求加入了新的枚举
indeterminate = 0xFFFFFFFF};
这种新需求会带来整个项目可能都需要重新编译。这是一个令人讨厌的情况,但如果使用C++11的前置声明则可以免疫这种弊端。示例代码如下:
enum class Status; //前置声明
void continueProcessing(Status s); //取用前置声明的枚举型别
如果采用上述方式,则在Status定义发生改变的时候,不需要重新编译,并且continueProccessing由于并未用到上述新增定义的时候,也同样无需重新编译。
造成这种现象的原因其实是由于以下规则:
限定作用域的枚举型别的底层型别是 int。
如果默认型别不满足要求,可以推翻它,例如如下代码:
enum class Status; //底层型别是int
enum class Status: std::uint32_t; //底层型别是uint32_t
正是由于这种特点,所以不限定型别的枚举可以前置声明,因为底层型别是确定的。更有甚者,不限定型别的枚举通过这种方式也可以前置声明。例如如下代码:
enum Color: std::uint8_t; //不限定范围的枚举也能前置声明了
enum class Status: std::uint32_t { //在定义中指定也是可以的
good = 0,
failed = 1,
incomplete = 100,
corrupt = 200,
audited = 500,
indeterminate = 0xFFFFFFFF
}
特例:在有些情况下,不限定范围枚举还是有意义的
当C++11中使用std::tuple型别的各个域时候,不限定范围枚举比较方便。
实例代码如下:
using UserInfo //型别别名,参见Item 9
= std::tuple<std::string, //名字
std::string, //电子邮件
std::size_t>; //声望值
//用法
UserInfo uInfo; //std::tuple型别对象
...
auto val = std::get<1>(uInfo); //取用域1的值
//这里用1来表示则显得十分不清晰了,而采用一个不限范围的
//枚举类型则会好很多
enum UserInfoFields {uiName, uiEmail, uiReputation};
UserInfo uInfo;
...
auto val = std::get<uiEmail>(uInfo); //多么直观,取用的电子邮件
但是如果要使用限定作用域的枚举型别版本的对应代码,就啰嗦很多,示例如下:
enum class UserInfoFields {uiName, uiEmail, uiReputation};
UserInfo uInfo;
...
auto val =
std::get<static_cast<std::size_t>(UserInfoFields::uiEmail)>
(uInfo);
如果想不这么啰嗦,则需要些一个函数,以枚举量为形参并返回其对应的std::size_t
型别的值,这样不太容易,因为std::get
是个模板,而传入的值是个模板形参,所以这个将枚举量变换成std::size_t
型别值的函数必须在编译期就计算出来。那么必须用constexpr
,并且返回值还需要泛化,不能返回std::size_t
而需要返回枚举底层型别,而这个型别需要用std::underlying_type
型别特征取得。最后还需要声明为noexcept
。那么最终的形式差不多是这样的:
//C++11
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,复杂写法
template<typename E>
constexpr std::underlying_type_t<E>
toUType(E enumerator) noexcept
{
return
static_cast<std::underlying_type_t<E>>(enumerator);
}
//C++14,简单写法
template<typename E>
auto toUType(E enumerator) noexcept
{
return
static_cast<std::underlying_type_t<E>>(enumerator);
}
//在我们有这个函数的基础上,终于可以这样访问:
auto val = std::get<toUType(UserInfoFields::uiEmail)>(uInfo);
可以发现,即便这样写依旧很复杂,这个时候无限定作用域的枚举会好一些。但除此之外,绝大多数情况还是要不要用这种技术,毕竟它无限定作用域枚举
发明的时候,最高端的数据通信还是用的2400波特率的调制解调器。
要点速记 |
---|
1. C++98风格的枚举型别,现在称为不限定范围枚举。 |
2. 限定作用域范围枚举仅在枚举型别内可见。他们只能通过强转进行转换。 |
3. 限定作用域/不限定作用域枚举都支持底层型别指定。限定作用域枚举底层型别默认是int,不限定作用域没有默认。 |
4. 限定作用域枚举总是可以前置声明,不限定作用域枚举只有指定了默认型别才能前置声明。 |