C++ 枚举全解析:不断演进的enum
一、枚举快速入门
1.1 枚举是什么
枚举(enum,全称 enumeration)是 C/C++ 提供的一种用户自定义数据类型。用户定义一组具名的整数标识符(称为“枚举集”、“枚举器常量”、“枚举器”或“成员”),枚举类型的变量存储该类型所定义的枚举集的值之一。
在某些场景下使用枚举更具可读性和可维护性,例如:表示状态机、定义程序错误码、位掩码等。
1.2 枚举的使用
语法:
1 | enum [identifier] |
identifier : 可选,枚举类型名,如果没有指定,则为匿名枚举
enumerator-list:定义枚举集的成员
下面的规则适用于枚举集的成员:
- 第一个枚举常量的默认值为0,后续的枚举常量值默认为上一个枚举常量值+1
- 枚举集可以包含重复的常量值。例如,可以将值 0 与两个不同的标识符关联,但不推荐这样做
- 一个枚举集下所有枚举常量类型相同,在 ANSI C 中,类型始终为
int
推荐枚举常量名为全大写。
示例1:使用枚举表示星期
1 | enum DAY |
默认情况下,值 0 与 saturday 关联。 标识符 sunday 将显式设置为 0。 默认情况下,将为剩余标识符提供从 1 到 5 的值。
示例2: 使用枚举表示交通信号灯
1 |
|
通过上面这个例子我们发现:枚举常量的值是不可变的,枚举变量的值是可以变的。
1.3 枚举存在的问题
1.3.1 作用域污染
枚举没有自己的作用域,编译器会把内部定义的枚举常量标识符导出到该枚举所在的作用域,这样虽然方便使用,但会导致名称冲突,造成作用域污染问题。
下面是一个作用域污染的示例:
1 |
|
在C++中解决此类问题的一个办法是使用命名空间,例如:
1 | namespace kxl |
1.3.2 隐式转换存在风险
虽然枚举存在一定的安全检查功能,一种枚举类型下的成员不允许分配到另外一种枚举类型的变量中。但C/C++中枚举支持隐式转换,即一个枚举类型可以隐式转换为整型。甚至C语言支持整形可以隐式转换为枚举类型。隐式转换会带来一系列潜在的安全问题,从而导致不易察觉的错误。
1 |
|
我们看到b = x < y没有编译报错是因为枚举类型先被隐式转换为整型,然后才进行比较。对于int z = STUDENT也是一样的。
下面代码在C++中编译错误,但在C语言中编译通过,因为C语言整形可以隐式转换为枚举类型,该代码带来的破坏性是不可预知的。
1 | enum School s = CHAIRMAN; // CHAIRMAN隐式转为整数2,2被隐式转为枚举常量PRINCIPAL,可能造成灾难性破坏。 |
1.3.3 无法指定枚举的底层类型
枚举的底层类型决定了它所占用的内存大小和能表示的数值范围。在 ANSI C 中,底层类型为int。不同的编译器对于相同枚举类型可能会使用不同的底层类型,这会造成可移植性问题。
1 | enum Color |
如果一个编译器对Color底层使用有符号数,那么值为*-1*。如果另一个编译器使用无符号数,那么值为4294967295。
二、现代C++
2.1 C++ 11
由于枚举存在上述所说的种种问题,在C++ 11中提出强枚举类型,它具有更严格的类型检查和作用域控制。另外,为了保证代码的兼容性,也保留了C++ 11之前枚举的特性,我们称为传统枚举。强枚举类型定义非常简单,只需要在枚举定义的enum关键字之后加上class或struct关键字即可。
强枚举类型具备以下新特性:
枚举有作用域,即枚举名, 不能在外部直接访问枚举常量
枚举类型不会隐式转换为整型。
可以指定枚举的底层类型,底层类型默认为
int不允许匿名,即必须要有identifier
语法:
1 | enum [class|struct] [identifier] [: type] |
C++ 11中传统枚举也可以指定底层类型,但如果未指定,底层类型由编译器选择,这是为了兼容C++ 11之前代码。
下面是一个演示强枚举类型的示例:
1 |
|
C23支持指定枚举的底层类型。
2.2 C++ 17
C++ 17 允许对有底层类型的枚举对象使用列表初始化。强枚举类型天生符合条件,因为它默认的底层类型为int,而传统枚举必须显式指定底层类型才符合条件。
1 |
|
如果定义Color c1{3.14}会编译失败,因为列表初始化禁止缩窄转换。另外我们注意到c与t的值为5,但是它们对应的枚举类型中没有哪一个枚举常量的值为5。C++ 11为了枚举的安全性,对枚举做的严格限制被打破了,那么C++ 17为什么要这么做?
现在假设一个场景,我们需要一个新整数类型,该类型不能与其他类型做隐式转换,从而消除无意的隐式转换导致细微错误的可能。显然使用typedef的方法是不行的。另外,虽然通过定义一个类的方式达到这个目的,但是这个方法需要编写大量的代码来重载运算符,也不是一个理想的方案。所以,C++的专家把目光投向了可以指定底层类型的枚举身上,枚举的特性几乎完美地符合以上要求,除了初始化时需要强制类型转换。于是,C++ 17为有底层类型的枚举放宽了初始化的限制,允许使用列表初始化。在C++ 17种新引入的std::byte类型就是用这种方法,下面是简单模拟实现。
1 |
|
2.3 C++ 20
C++ 20扩展了using功能,它可以引入强枚举类型的成员。在一些情况下,这样做会让代码更加简洁易读。
using enum 强制枚举类型名 : 将全部枚举常量引入
1 | enum class TrafficLight |
using 强制枚举类型::枚举常量标识符 : 将指定枚举常量引入
1 | enum class TrafficLight |
参考
谢丙堃 <<现代C++语言核心特性解析>>
microsoft C++ 语言文档