C++ 枚举全解析:不断演进的enum

一、枚举快速入门

1.1 枚举是什么

​ 枚举(enum,全称 enumeration)是 C/C++ 提供的一种用户自定义数据类型。用户定义一组具名的整数标识符(称为“枚举集”、“枚举器常量”、“枚举器”或“成员”),枚举类型的变量存储该类型所定义的枚举集的值之一。

​ 在某些场景下使用枚举更具可读性和可维护性,例如:表示状态机、定义程序错误码、位掩码等。

1.2 枚举的使用

语法:

1
2
3
4
enum [identifier]
{
enumerator-list;
};

identifier : 可选,枚举类型名,如果没有指定,则为匿名枚举

enumerator-list:定义枚举集的成员

下面的规则适用于枚举集的成员:

  • 第一个枚举常量的默认值为0,后续的枚举常量值默认为上一个枚举常量值+1
  • 枚举集可以包含重复的常量值。例如,可以将值 0 与两个不同的标识符关联,但不推荐这样做
  • 一个枚举集下所有枚举常量类型相同,在 ANSI C 中,类型始终为int

推荐枚举常量名为全大写

示例1:使用枚举表示星期

1
2
3
4
5
6
7
8
9
10
enum DAY            
{
SATURDAY,
SUNDAY = 0,
MONDAY,
TUESDAY,
WEDNESDAY,
THURSDAY,
FRIDAY
};

默认情况下,值 0 与 saturday 关联。 标识符 sunday 将显式设置为 0。 默认情况下,将为剩余标识符提供从 1 到 5 的值。

示例2: 使用枚举表示交通信号灯

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
#include <iostream>

enum TrafficLight
{
RED, // 值为0
YELLOW = 2, // 值为2
GREEN // 值为3
};

void checkLight(TrafficLight light)
{
switch (light)
{
case RED:
std::cout << "Stop!" << std::endl;
break;
case YELLOW:
std::cout << "Get ready!" << std::endl;
break;
case GREEN:
std::cout << "Go!" << std::endl;
break;
default:
std::cout << "Invalid signal!" << std::endl;
}
}

int main()
{
TrafficLight signal = RED; // 如果为C语言, 定义枚举变量需要在枚举类型前加 enum 关键字
checkLight(signal); // 输出 "Stop!"

signal = GREEN;
checkLight(signal); // 输出 "Go!"

RED = 4; // error

return 0;
}

通过上面这个例子我们发现:枚举常量的值是不可变的,枚举变量的值是可以变的

1.3 枚举存在的问题

1.3.1 作用域污染

枚举没有自己的作用域,编译器会把内部定义的枚举常量标识符导出到该枚举所在的作用域,这样虽然方便使用,但会导致名称冲突,造成作用域污染问题。

下面是一个作用域污染的示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <iostream>

enum Color // 内部枚举常量被导出到Color所在作用域,即全局作用域
{
RED,
GREEN,
BLUE
};

enum TrafficLight // 内部枚举常量被导出到TrafficLight所在作用域,即全局作用域
{
RED, // ⚠️ 和 Color::RED 冲突
YELLOW,
GREEN // ⚠️ 和 Color::GREEN 冲突
};

int main()
{
int color = RED; // ❌ 编译错误,RED 不明确(Color::RED or TrafficLight::RED?)

return 0;
}

在C++中解决此类问题的一个办法是使用命名空间,例如:

1
2
3
4
5
6
7
8
9
namespace kxl
{
enum TrafficLight
{
RED,
YELLOW,
GREEN
};
}

1.3.2 隐式转换存在风险

虽然枚举存在一定的安全检查功能,一种枚举类型下的成员不允许分配到另外一种枚举类型的变量中。但C/C++中枚举支持隐式转换,即一个枚举类型可以隐式转换为整型。甚至C语言支持整形可以隐式转换为枚举类型。隐式转换会带来一系列潜在的安全问题,从而导致不易察觉的错误。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include <stdio.h>

enum School
{
STUDENT,
TEACHER,
PRINCIPAL
};

enum Company
{
EMPLOYEE,
MANAGER,
CHAIRMAN
};

int main()
{
School x = STUDENT;
Company y = MANAGER;
bool b = x >= PRINCIPAL; // 编译通过,合理,相同枚举类型比较
b = x < y; // 编译通过,但不合理,它们没有可比性
int z = STUDENT; // 编译通过

return 0;
}

我们看到b = x < y没有编译报错是因为枚举类型先被隐式转换为整型,然后才进行比较。对于int z = STUDENT也是一样的。

下面代码在C++中编译错误,但在C语言中编译通过,因为C语言整形可以隐式转换为枚举类型,该代码带来的破坏性是不可预知的。

1
2
enum School s = CHAIRMAN; // CHAIRMAN隐式转为整数2,2被隐式转为枚举常量PRINCIPAL,可能造成灾难性破坏。
x = 10; // 10被隐式转为枚举类型,School内不存在值为10的枚举常量,灾难性破坏

1.3.3 无法指定枚举的底层类型

枚举的底层类型决定了它所占用的内存大小和能表示的数值范围。在 ANSI C 中,底层类型为int。不同的编译器对于相同枚举类型可能会使用不同的底层类型,这会造成可移植性问题。

1
2
3
4
enum Color
{
RED = 0xFFFFFFFF
}

如果一个编译器对Color底层使用有符号数,那么值为*-1*。如果另一个编译器使用无符号数,那么值为4294967295

二、现代C++

2.1 C++ 11

​ 由于枚举存在上述所说的种种问题,在C++ 11中提出强枚举类型,它具有更严格的类型检查和作用域控制。另外,为了保证代码的兼容性,也保留了C++ 11之前枚举的特性,我们称为传统枚举。强枚举类型定义非常简单,只需要在枚举定义的enum关键字之后加上classstruct关键字即可。

强枚举类型具备以下新特性:

  • 枚举有作用域,即枚举名, 不能在外部直接访问枚举常量

  • 枚举类型不会隐式转换为整型。

  • 可以指定枚举的底层类型,底层类型默认为int

  • 不允许匿名,即必须要有identifier

语法:

1
2
3
4
enum [class|struct] [identifier] [: type]
{
enumerator-list;
};

C++ 11中传统枚举也可以指定底层类型,但如果未指定,底层类型由编译器选择,这是为了兼容C++ 11之前代码。

下面是一个演示强枚举类型的示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include <iostream>

enum class Color : uint8_t // 指定底层类型为uint8_t
{
RED,
GREEN,
BLUE
};

enum class TrafficLight // 底层类型默认使用int
{
RED, // 该枚举常量在作用域TrafficLight中
YELLOW,
GREEN // 该枚举常量在作用域TrafficLight中
};

int main()
{
Color x1 = RED; // 错误,必须指定RED的作用域
Color x2 = Color::RED; // 正确
TrafficLight y1 = Color::RED; // 错误,类型不匹配。
TrafficLight y2 = TrafficLight::RED; // 正确
bool b = x2 > y2; // 错误,强枚举类型不会隐式转换为整型。
int i = x2; // 错误,强枚举类型不会隐式转换为整型。
int j = static_cast<int>(x2); // 正确,可以强制类型转换

return 0;
}

C23支持指定枚举的底层类型。

2.2 C++ 17

C++ 17 允许对有底层类型的枚举对象使用列表初始化。强枚举类型天生符合条件,因为它默认的底层类型为int,而传统枚举必须显式指定底层类型才符合条件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <iostream>

enum class Color
{
Red,
Green,
Blue
};

enum TrafficLight : u_int8_t
{
RED, // 值为0
YELLOW = 2, // 值为2
GREEN // 值为3
};

int main()
{
Color c{5}; // 编译成功,强枚举类型,没有显式指定了底层类型,则使用默认类型int
Color c1 = 5; // 编译失败
Color c2 = {5}; // 编译失败
Color c3(5); // 编译失败

TrafficLight t{5}; // 编译成功,虽然为传统枚举,但显式指定了底层类型
}

如果定义Color c1{3.14}会编译失败,因为列表初始化禁止缩窄转换。另外我们注意到ct的值为5,但是它们对应的枚举类型中没有哪一个枚举常量的值为5。C++ 11为了枚举的安全性,对枚举做的严格限制被打破了,那么C++ 17为什么要这么做?

现在假设一个场景,我们需要一个新整数类型,该类型不能与其他类型做隐式转换,从而消除无意的隐式转换导致细微错误的可能。显然使用typedef的方法是不行的。另外,虽然通过定义一个类的方式达到这个目的,但是这个方法需要编写大量的代码来重载运算符,也不是一个理想的方案。所以,C++的专家把目光投向了可以指定底层类型的枚举身上,枚举的特性几乎完美地符合以上要求,除了初始化时需要强制类型转换。于是,C++ 17为有底层类型的枚举放宽了初始化的限制,允许使用列表初始化。在C++ 17种新引入的std::byte类型就是用这种方法,下面是简单模拟实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <iostream>

enum class byte : unsigned char
{
};

int main()
{
byte b1{3}; // 正确
byte b2{5}; // 正确
b1 = b2; // 正确

b1 = 10; // 错误
int i = b1; // 错误,枚举不允许隐式转换
}

2.3 C++ 20

C++ 20扩展了using功能,它可以引入强枚举类型的成员。在一些情况下,这样做会让代码更加简洁易读。

using enum 强制枚举类型名 : 将全部枚举常量引入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
enum class TrafficLight
{
RED, // 值为0
YELLOW = 2, // 值为2
GREEN // 值为3
};

void checkLight(TrafficLight light)
{
using enum TrafficLight; // 将全部枚举常量引入,后续使用无需指定命名空间

switch (light)
{
case RED:
std::cout << "Stop!" << std::endl;
break;
case YELLOW:
std::cout << "Get ready!" << std::endl;
break;
case GREEN:
std::cout << "Go!" << std::endl;
break;
default:
std::cout << "Invalid signal!" << std::endl;
}
}

using 强制枚举类型::枚举常量标识符 : 将指定枚举常量引入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
enum class TrafficLight
{
RED, // 值为0
YELLOW = 2, // 值为2
GREEN // 值为3
};

void checkLight(TrafficLight light)
{
using TrafficLight::RED; // 将RED引入,后续使用无需指定命名空间

switch (light)
{
case RED:
std::cout << "Stop!" << std::endl;
break;
case TrafficLight::YELLOW:
std::cout << "Get ready!" << std::endl;
break;
case TrafficLight::GREEN:
std::cout << "Go!" << std::endl;
break;
default:
std::cout << "Invalid signal!" << std::endl;
}
}

参考

谢丙堃 <<现代C++语言核心特性解析>>

microsoft C++ 语言文档