为什么我不推荐用 enum class 作为强类型的整数?

C++11 之后 enum 的新增功能

enum class 是 C++11 提供的功能,为了更好理解后文的内容,我们先看看 C++11 之后 enum 有什么变化。参考资料见 https://en.cppreference.com/w/cpp/language/enum

有作用域枚举

有作用域枚举(Scoped enumerations)使用 enum class|struct ClassName 声明,以区别于原来的无作用域枚举。无作用域枚举的枚举量可以直接在外围名字空间中访问,当枚举类有名字且 C++ 版本至少为 C++11 时,可以通过 枚举名::枚举量 访问;有作用域枚举只能通过 枚举名::枚举量 访问。

有作用域枚举的默认底层类型是 int

例子:

void example() {
    enum class Color { red, green = 20, blue };
    Color r = Color::blue;
}

显式指定底层类型

C++11 可以显式指定枚举类的底层类型(underlying type),提高了代码可移植性。此前枚举底层类型是实现定义。

例子:

enum : int {
  APPLE,
};

笼统枚举声明

笼统枚举声明(Opaque enum declaration)就是不写枚举项,但是声明一个枚举类型,同时枚举类的底层类型也在声明的位置上确定下来。因此,笼统枚举声明只能用来声明有作用域枚举,或者用来声明有显式底层类型的无作用域枚举。

例子:

enum byte : unsigned char;

从整数类型到固定底层类型枚举类的显式转换(C++17)

在 C++17 之后,一个枚举类有了固定底层类型(不再是实现定义底层类型,包括有作用域枚举和显式声明了底层类型的枚举类型这两种情况),就能从整数类型显式构造,不过要保证这个构造不会使得整数表示范围变窄。

Cppreference 的网页说了很多要求,但是从形式上来看就像枚举类对每个 non-narrowing 的整数类型有了 explicit 的构造函数(枚举类不是类,本身没有构造函数):

enum byte : unsigned char {}; // byte is a new integer type; see also std::byte (C++17)
byte b{42};        // OK as of C++17 (direct-list-initialization)
byte c = {42};     // error
byte d = byte{42}; // OK as of C++17; same value as b
byte e{-1};        // error

在 libstdc++ 中,std::byte 也是用这种思路实现的。

using enum(C++20)

通过 using enum 声明可以像把枚举类当成名字空间一样,引入其中的枚举量定义。

其他要注意的事情

枚举量的底层值是可以重复的

枚举值从上一项开始递增加 1(第一个枚举量的默认值为 0)。因此,不同枚举量的值是有可能重复的,无论是有作用域枚举还是无作用域枚举都是这样。

enum class A { a, b, c = 0, d = a + 2 }; // 定义 a = 0, b = 1, c = 0, d = 2
static_assert(A::a == A::c);

枚举类型和整数类型之间的转换关系

  1. 枚举转整数:无作用域枚举量可以转换成整数类型,有作用域枚举不能。
  2. 整数转枚举:从 C++17 开始,有固定底层类型的枚举可以通过包含整数的花括号显式构造。

可以看 Compiler Explorer 链接

回答标题的问题

这是一种个人选择。在接口上,使用 enum class 保证了强类型,但是这仅在枚举只拿来当枚举用的情况下比较合适。如果要使用的枚举类要参与算术运算,那么类型转换写起来很麻烦。

首先,现在一般不会有人喜欢无作用域 enum,那么我们讨论的就是有作用域 enum,它无法转换成整数类型。每次做计算之前都需要使用强制类型转换,不如直接使用只包含一个整数元素的结构体:

struct Id {
    int value;
};

或者,不要用这么多的抽象,直接用整数表示 id,只是这样做就舍弃了类型检查的优势了。

我此前印象比较深刻的是写 parser 的时候用了 enum class 表示 id,结果类型转换非常麻烦,可能一些必须为整数的物理量也适用这一点?

为什么 id 需要去做计算?这时写起来为什么麻烦?

  1. for 循环遍历 id 集合。在 C++17 之后,可以从整数构造有固定底层类型的枚举类,这个就不成问题了。
  2. 某个场景下,你设计的 id 相互之间保持了某种逻辑关系(不对调用处开放),这会需要用整数运算做一些 tricks。
  3. 有两个不同的 id 类别,在满足条件时,他们的值相等。(比如 Linux 中进程组中的第一个进程满足:进程号和进程组号相等。我们假设它们类型不同,就会遇到这种情况。不过这个例子有点牵强,没有考虑现实情况:Linux 中线程号、进程号、进程组号、会话号等都是用的 pid_t 类型。)进行这种枚举转换时必须使用强制类型转换(static_cast)。