每周技巧 #86:用类枚举

本节阅读量:

本文翻译自 Abseil 官网的 Tip of the Week #86: Enumerating with Class

原文最初作为 totw/86 发布于 2015 年 1 月 5 日。

作者:Bradley White

“展现 class,……并显示 character。”– Bear Bryant

枚举,或者简单说 enum,是一种可以保存一组指定整数之一的类型。这个集合中的某些值可以被赋予名字,称为枚举项。

非限定作用域枚举

C++ 程序员会熟悉这个概念,但在 C++11 之前,枚举有两个明显缺点:枚举项名称会:

  • 与 enum 类型处于同一作用域;
  • 隐式转换为某种整数类型的值。

所以,在 C++98 中……

1
2
3
enum CursorDirection { kLeft, kRight, kUp, kDown };
CursorDirection d = kLeft; // OK:枚举项在作用域内
int i = kRight;            // OK:枚举项转换为 int

但是……

1
2
// 错误:kLeft 和 kRight 重复声明
enum PoliticalOrientation { kLeft, kCenter, kRight };

C++11 以一种方式修改了非限定作用域 enum 的行为:枚举项现在局部属于 enum,但为了向后兼容,仍会导出到 enum 所在作用域。

所以,在 C++11 中……

1
2
CursorDirection d = CursorDirection::kLeft;  // C++11 中 OK
int i = CursorDirection::kRight;             // OK:仍然转换为 int

PoliticalOrientation 的声明仍然会引发错误。

限定作用域枚举

隐式转换为整数已经被观察到是 bug 的常见来源,而枚举项与 enum 处于同一作用域造成的命名空间污染,也会在大型、多库项目中带来问题。为了解决这两个问题,C++11 引入了一个新概念:限定作用域 enum

由关键字 enum class 引入的限定作用域 enum 中,枚举项:

  • 只局部属于 enum(不会导出到 enum 所在作用域);
  • 不会隐式转换为整数类型。

所以,(注意额外的 class 关键字)……

1
2
3
4
enum class CursorDirection { kLeft, kRight, kUp, kDown };
CursorDirection d = kLeft;                    // 错误:kLeft 不在此作用域
CursorDirection d2 = CursorDirection::kLeft;  // OK
int i = CursorDirection::kRight;              // 错误:没有转换

并且……

1
2
// OK:kLeft 和 kRight 局部属于各自的 scoped enum
enum class PoliticalOrientation { kLeft, kCenter, kRight };

这些简单变化消除了普通枚举的问题,因此所有新代码都应优先使用 enum class。

使用限定作用域 enum 确实意味着,如果你仍然想要转换到整数类型,就必须显式转换(例如在记录枚举值日志时,或对类似 flag 的枚举项使用位运算时)。不过,用 std::hash 做哈希仍然会继续工作(例如 std::unordered_map<CursorDirection, int>)。

枚举底层类型

C++11 还引入了为两类枚举指定底层类型的能力。以前 enum 的底层整数类型由枚举项的符号和大小决定,但现在我们可以显式指定。例如:

1
2
// 使用 "int" 作为 CursorDirection 的底层类型
enum class CursorDirection : int { kLeft, kRight, kUp, kDown };

因为这个枚举项范围很小,如果我们希望在存储 CursorDirection 值时避免浪费空间,也可以改为指定 char

1
2
// 使用 "char" 作为 CursorDirection 的底层类型
enum class CursorDirection : char { kLeft, kRight, kUp, kDown };

如果枚举项值超过底层类型的范围,编译器会报错。

结论

新代码中优先使用 enum class。这样可以减少命名空间污染,并且可能避免隐式转换中的 bug。

1
enum class Parting { kSoLong, kFarewell, kAufWiedersehen, kAdieu };

每周技巧 #77:临时对象、移动和拷贝

上一节

每周技巧 #88:初始化:=、() 和 {}

下一节