Effective Modern C++ Item 10 优先选用限定作用于的枚举型别,而非不限作用于的枚举型别

先来一个通用规则:

如果在一对大括号里声明一个名字,则该名字的可见性救兵限定在括号括起来的作用域内。

但这个规则有个例外 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. 限定作用域枚举总是可以前置声明,不限定作用域枚举只有指定了默认型别才能前置声明。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值