条款10:优先选用限定作用域的枚举型别,而非不限作用域的枚举型别

ZaynPei Lv6

C++98 风格的枚举(现在称为不限作用域的枚举型别)存在两个主要缺陷。C++11 引入的限定作用域的枚举型别(也称“枚举类”)则直接解决了这两个问题。

了解枚举

在 C++ 中,枚举 (Enumeration, 简称 enum) 是一种用户自定义的数据类型,它允许我们为一组相关的整型常量赋予具有描述性的名称

使用枚举的主要目的是增强代码的可读性和类型安全,避免在代码中直接使用“魔术数字”(Magic Numbers,即未经解释的字面常量)。

C++ 语言中存在两种截然不同的枚举类型:

  • 不限定作用域的枚举(Unscoped Enumerations,也称为 C 风格枚举)
  • 限定作用域的枚举(Scoped Enumerations,C++11 引入,使用 enum class 关键字)

不限定作用域的枚举 (C 风格)

这是从 C 语言继承而来的传统枚举, 它使用 enum 关键字定义。

1
2
3
4
5
6
7
8
9
10
11
12
13
// 定义一个表示颜色的枚举
enum Color {
RED, // 默认值为 0
GREEN, // 默认值为 1
BLUE // 默认值为 2
};

enum Status {
Pending = 10,
Processing, // 未指定,值为前一个 + 1,即 11
Completed = 20,
Failed // 值为 21
};
这里我们定义了一个名为 Color 的新类型。RED、GREEN 和 BLUE 是这个类型的枚举器(Enumerators)。其赋值的默认规则是,第一个枚举器(RED)的值为 0,后续枚举器依次递增 1; 我们也可以为枚举器指定具体的值,如 Pending = 10, Completed = 20。

尽管 C 风格枚举提高了可读性,但它在 C++ 中存在两大严重问题:

  1. 缺陷一:作用域污染 (Scope Pollution): C 风格枚举的枚举器会泄漏到其外围作用域(例如,全局作用域或所在的命名空间/类)。
    1
    2
    3
    4
    5
    enum Color { RED, GREEN, BLUE };
    enum TrafficLight { RED, YELLOW, GREEN }; // 编译错误!

    // 错误原因:RED 和 GREEN 在同一作用域内被重复定义了。
    // 编译器无法区分 Color 的 RED 和 TrafficLight 的 RED。
  2. 缺陷二:弱类型与隐式转换 (Weak Typing): C 风格枚举的枚举器会自动地、隐式地转换为整型(如 int)。这破坏了类型安全。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    Color myColor = RED;
    int colorValue = myColor; // 合法,myColor (值为 0) 被隐式转换为 int 0

    if (myColor == 0) { // 合法,但不推荐
    // ...
    }

    // 更糟糕的是,不同枚举类型之间也可能进行比较(只要它们底层值相同)
    TrafficLight light = RED; // 假设 TrafficLight 的 RED 也是 0
    if (myColor == light) { // 编译通过!
    // 这种比较在逻辑上是无意义的,但编译器允许
    }

限定作用域的枚举 (C++11 enum class)

为了解决上述所有问题,C++11 引入了“限定作用域的枚举”,也称为“枚举类”。这是在现代 C++ 中定义枚举的首选方式。它使用 enum class 关键字(或者等效的 enum struct)来定义。

1
2
3
4
5
6
7
8
9
10
11
enum class Color {
RED,
GREEN,
BLUE
};

enum class TrafficLight {
RED,
YELLOW,
GREEN
};

它的优势一是强作用域 (Strongly Scoped): enum class 的枚举器被严格限制在枚举类型的花括号内,不会泄漏到外部作用域。不过也正因此, 访问这些枚举器时,必须使用枚举类型名称作为限定符。

1
2
3
4
5
6
7
8
// 必须通过类型名::枚举器 来访问
Color myColor = Color::RED;
TrafficLight light = TrafficLight::RED;

// 编译通过!
// Color::RED 和 TrafficLight::RED 位于不同作用域,互不干扰。

Color c = RED; // 编译错误!RED 不在当前作用域中。

优势二是强类型,禁止隐式转换为整型. 这是 enum class 提供的最关键的类型安全保证。

1
2
3
4
5
6
7
8
9
10
11
12
Color myColor = Color::RED;

int colorValue = myColor; // 编译错误!
// 无法将 Color 类型隐式转换为 int

if (myColor == 0) { // 编译错误!
// 无法在 Color 类型和 int 类型之间进行比较
}

// 不同枚举类型之间也不能比较
if (myColor == TrafficLight::RED) { // 编译错误!
}

如果确实需要将枚举值转换为整数,我们必须使用显式类型转换(通常是 static_cast)。强制使用 static_cast 表明程序员是有意图地要将这个具有强类型的枚举值转换为一个普通的整数,这使得代码意图更加清晰。

两者的底层类型

在C++98的默认情况下,编译器会为枚举选择一个足够大的整数类型(如 int)来存储所有枚举值, 但这必须要求枚举定义完成之后(也就是C++98无法实现枚举的前向声明)。但在 C++11 中,我们可以为两种枚举(C 风格和 enum class)显式指定底层存储类型, 主要的好处在于:

  • 内存控制: 当枚举值范围很小时(例如 0-255),可以指定一个小的类型(如 std::uint8_t,即无符号 8 位整数),这在定义大型数组或内存敏感的结构体时非常重要。
  • API 兼容性: 确保与需要特定整数宽度(如 32 位)的 C API 或库函数兼容。
  • 前向声明: enum 如果指定了底层类型,就可以被前向声明(在头文件中提前声明类型,而在 .cpp 中定义具体内容)。
    • 限定作用域(enum class)的枚举默认的底层类型是 int , 由于底层类型总是已知的(要么是默认的 int,要么是用户指定的),因此它们总是可以被前置声明(总之限定作用域的枚举更优越) 。
    • 不限作用域(enum)的枚举没有默认的底层类型, 因此必须显式指定了底层类型,不限作用域的枚举才可以被前置声明 。
      1
      2
      3
      4
      5
      6
      7
      // 语法:enum class 类型名 : 底层整数类型 { ... };
      // 这个枚举将使用一个 8 位的无符号字节来存储
      enum class CompactColor : std::uint8_t {
      RED,
      GREEN,
      BLUE
      };
On this page
条款10:优先选用限定作用域的枚举型别,而非不限作用域的枚举型别