Effective Modern C++ 条款10 比起unscoped enums更偏爱scoped enums

比起unscoped enums更偏爱scoped nums

作为一个通用的规则,在大括号内声明一个名字就限制了名字的作用域在括号之内。这个规则不适用于C++98的枚举。这种枚举的枚举值的作用域与枚举enum的作用域相同:

enum Color { black, white, red };  // black while red的作用域与Color相同

auto white = false;   // 错误,当前作用域已经声明了white

事实上,这些枚举的值会从它们的枚举(enum)中泄露出来,引出了这种enum的正式风格:unscoped 。在C++11中,有它的相对物,scoped enum,不会造成这种泄露值的问题:

enum class Color { black, white, red };  // black white red作用域在Color内

auto white = false;  // 没有错误,没有其他的“white”在当前作用域

Color c = white;  // 错误,当前作用域没有“white”这个枚举的值
                   // 只有bool类型的“white”

Color c = Color::white;      // 正确

auto c = Color::white;    // 也正确(根据条款5的建议)

因为scoped enum通过“enum class”来声明,所以它们指代的是枚举类。

scoped enum减少命名空间污染已经足够说服你用scoped enum代替unscoped enum了,不过scoped enum还有一个让你不得不使用它的优势:它枚举的名字具有更健壮的类型。unscoped enum枚举的值隐式地转换为整型数类型(或者浮点数类型)。一些语义上的歪曲也是完全可行的,如下:

enum Color { black, white, red };   // unscoped enum

std::vector<std::size_t> primeFactors(std::size_t);  // 返回质因数

Color c = red;
...
if (c < 14.5) {   // 比较Color和浮点数
  auto factors = primeFactors(c);  //计算Color的质因数
  ...
}

但是,在enum之后加个classunscoped enum转换成scoped enum,这样就是另一个不同的故事啦。scoped enum枚举的值无法被隐式转换为其他类型:

enum class Color { black, white, red };  // scoped enum

Color c = Color::red;
...
if (c < 14.5) {  // 错误, 无法与Color和double比较
  auto factors = primeFactors(c);   // 错误
  ...                                //  无法把Color传递给参数为size_t的函数
}

如果你确实想要把Color转换为不同的类型,你应该说明你想要的类型——使用cast

if (static_cast<double>(c) < 14.5) {
  auto factors = primeFactors(static_cast<std::size_t>(c));
  ...
}

好像scoped enum比起unscoped enum还有第三个优点,scoped enum可以前向声明(forward-declaration),它可以不带值地声明枚举的名字:

enum Color;    // 错误

enum class  Color;     // 正确

不过这是误导人的。在C++11中,unscoped enum也可以前向声明了,不过需要做些额外的工作。这些额外的工作来源于每个enum值都有一个整数的基础类型(underlying type),而编译器要决定它们,例如:
enum Color { black, white, red };
编译器可能选择char作为潜在类型,因为只需表示3个值。但是,一些枚举值的范围会很大,例如:

enum Status { good = 0, 
              failed = 1, 
              incomplete = 100,
              corrupt = 200,
              indeterminate = 0xFFFFFFFF
             };

这里的值的范围从0~0xFFFFFFFF。就算是特殊的机器(char占32位),编译器也会选择比char类型更大的类型来表示Status的值。

为了高效实用内存,编译器一般选择足够表示枚举值范围的最小基础类型。在一些例子中,编译器会以速度为优化目的,而不是空间,此时编译器可能不会选择允许的最小基础类型,但它们肯定也想要优化空间使用。为了这个目的,C++98只允许定义enum(列出枚举值),而不允许声明enum。这使得编译器在使用 枚举值之前选择基础类型成为可能。

但是enum前向声明使得编译器无能为力。最可能实现的是增加编译依赖,再次思考Status:

enum Status { good = 0, 
              failed = 1, 
              incomplete = 100,
              corrupt = 200,
              indeterminate = 0xFFFFFFFF
              };

这个枚举好像会在一个系统中多处被使用,所以把它包含在头文件中。如果我们新引入了个枚举值:

enum Status { good = 0, 
              failed = 1, 
              incomplete = 100,
              corrupt = 200,
              audited = 500,  // 新引入
              indeterminate = 0xFFFFFFFF
              };`

这样的话可能整个系统都要重新编译了,就算只有一个子系统用到这个新引入的枚举值,就算只有一个函数用到。人们是非常厌恶这样的事情的。C++11的枚举前向声明能力可以消除这样的问题。例如,scoped enum这样的声明和一个接收它作为参数的函数:

enum class Status; // 前向声明

void continueProcessing(Status s);   // 使用前向声明的枚举

如果头文件包含这样的声明,那么修改Status的定义时,就不需要重新编译了。而且,如果Status(例如引入audited)被修改,continueProcessing函数的行为不会受到影响(因为函数没有使用audited),那么函数的实现也无需重新编译。

但是编译器使用枚举值之前需要知道枚举的基础类型。C++11的enum是如何侥幸使用前向声明的呢,C++98却不行。答案很简单:scoped enum的基础类型总是已知的,而对于unscoped enum需要指定它的类型。

scoped enum默认的基础类型是int
enum class Status; // 基础类型是int

如果默认的类型不适合你,你可以重写它:
enum class Status: std::uint32_t; // Status的基础类型是std::uint32

不管哪一种方式,编译器都会知道scoped enum枚举值的大小。

和上面一样,说明unscoped enum的基础类型,还可以前向声明:
enum Color: std::uint8_t; // unscoped enum前向声明

基础类型指定也可以在枚举定义时使用:

enum class Status: std::uint32_t { good = 0,
                                   failed = 1,
                                   incomplete = 100,
                                   corrupt = 200,
                                   audited = 500,
                                   indeterminate = 0xFFFFFFFF
                                  };

鉴于scoped enum可以避免命名空间污染,和不容易被无意义到隐式类型转换影响,如果告诉你不止一种情况下unscoped enum更有用,你可能会惊呆。这涉及到C++11的std::tuple。例如,我们有一个tuple持有名字、邮箱、在某社交网站上的名誉值:
using UserInfo = std::tuple<std::string, std::string, std::size_t>;
// 使用了using, 见条款9
// 参数分别是名字,邮箱,声名值

尽管注释中表明tuple每一区域表示的含义,但是你在单独的源码文件中遇到这样的代码,你会觉得毫无帮助:

UserInfo uInfo;  // tuple对象
...
auto val = std::get<1>(uInfo);  // 获取uInfo的第1个值

作为一个程序员,你有许多方法来追踪它。难道你真的要去记UserInfo的第1个值是邮箱吗?我觉得不用阿。使用unscoped enum来关联名字和数字就可以避免这个问题啦:

enum UserInfoFields {uiName, uiEmail, uiReputation };

UserInfo uInfo;
...
auto val = std::get<uiEmail>(uInfo);  // 取得邮箱的值

这样可行的原因是UserInfoFields的值隐式转换为std::size_t,也就是std::get需求的类型。

scoped enum实现就啰嗦了点:

enum class UserInfoFields { uiName, uiEmail, uiReputation };

UserInfo uInfo;
...
auto val = 
  std::get<static_cast<std::size_t>(UserInfoFileds::uiEmail)>         
     (uInfo);

这种冗长的代码可以写一个函数来减少,这个函数接受枚举值为参数,返回其相对的std::size_t值,但这有点复杂。std::get是一个模板,然后你传递给它的值是模板参数(注意它使用的是方括号,不是圆括号),所以这个转换枚举值到std::size_t的函数必须在编译期间得到结果。根据条款15,这意味着它是个constexpr函数。

事实上,它应该是个constexpr模板函数,因为它应该可以为多种枚举工作。然后如果我们想将它一般化,我们也应该把返回类型一般化。比起返回std::size_t,我们将返回枚举的基础类型,可以借助std::underlying_type得到。最后我们还要把它声明为noexcept(看条款14),因为我们知道它永远都不会抛出异常。最后就成了这个样子:

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中,“typename std::underlying_type::type”可以用时髦的“std::underlying_type_t”来代替(见条款9,本人省略了这部分):

template <typename E>
constexpr std::underlying_type_t<E>
  toUType(E enumerator) noexcept
{
  return static_cast<std::underlying_type_t<E>>(enumerator);
}

更时髦地返回auto在C++14中也是可行的:

template <typename E>
constexpr auto
  toUType(E enumerator) noexcept
{
  return static_cast<std::underlying_type_t<E>>(enumerator);
}

不管toUType怎么实现,toUType允许我们这样获取tuple中值:
auto val = std::get<toUType(UserInfoFields::uiEmail)>(uInfo);

这还是比unscoped enum写得多,不过这样还可以避免命名空间污染和枚举值的不经意的类型转换。在许多情况下,你可能觉得写多一些字母来避免一下枚举上的陷阱是值得理解的。

总结

需要记住的4点:

  • C++98风格的enum现在被称为unscoped enum
  • scoped enum的枚举值只在enum内可见,它们只能通过cast来转换成其他类型。
  • scoped enumunscoped enum都支持指定基础类型(underlying type),scoped enum的默认基础类型是intunscoped enum没有默认基础类型。
  • scoped enum总是可以前向声明,unscoped enum只有在它声明时指定了基础类型才可以前向声明。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章