对比C++中的传统枚举与强类型枚举类
在写项目的时候,我使用switch语句对怪物类型的枚举进行分拣,其中怪物类型的枚举常量与具体怪物的类名起了冲突,我这才仔细看了下enum和enum class的区别,整理到了笔记中,拿出其中一部分放在此博客里
对比C++中的传统枚举与强类型枚举类
一、背景信息
- 一般来说一个变量的标识符仅仅在其作用域内生效,但是传统C++的
enum
却特殊,只要有作用域包含了某个枚举类型,那么在这个父作用域内,这个枚举的变量名就生效了(即枚举量的名字泄露到了包含这个枚举类型的作用域内),此时在这个作用域内就不能有其他实体取相同的名字 - 在C++98中
enum
被称为不限范围的枚举型别,而C++11中新增了强枚举类enum class
,也称作限定作用域的枚举类
二、枚举enum
2.1 定义形式
- 枚举类型(Enumeration)是C++的一种派生数据类型,它是由用户定义的若干枚举常量的集合,其的定义格式为
- 定义枚举类型的主要目的就是增加程序的可读性
1
2
3
4
5
6
enum EnumTypeName
{
TypeA,
TypeB,
TypeC
};
- 花括号内的是枚举常量表,枚举常量又称枚举成员,枚举常量只能以标识符形式表示,而不能是整型或字符型
1
2
3
4
//enum Letter {'a','d','F','s','T'}; //枚举常量不能是字符常量
enum Letter {a, d, F, s, T};
//enum Year {2000,2001,2002,2003,2004,2005}; //枚举常量不能是整型常量
enum Year {y2000, y2001, y2002, y2003, y2004, y2005};
2.2 枚举常量的底层类型
- 编译系统为每个枚举常量指定一个整数值(整数类型由编译器决定,并不是确定的,这也是为什么
enum
不能被前置声明,因为无法分配内存),默认状态下其取值顺序就是所列举元素的排列顺序,从0
开始,直到Size-1
- 可以在定义枚举类型时为部分或全部枚举常量指定整数值,在指定值之前的枚举常量仍按默认方式取值,而指定值之后的枚举常量按依次加
1
的原则取值,各枚举常量的值可以重复
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
enum FruitSet
{
Apple, //0
Orange, //1
Banana=1, //1
Peach, //2
Grape //3
};
enum Week
{
Sun=7, //7
Mon=1, //1
Tue, //2
Wed, //3
Thu, //4
Fri, //5
Sat //6
};
三、枚举类enum class
3.1 定义形式
- 在
enum
的基础上加一个class
关键字即可,其它规则基本一致
1
2
3
4
enum class TypeName
{
//Enum List
};
3.2 指定底层类型
enum class
允许开发者为枚举成员指定底层类型
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//uint8_t与int16_t的支持
#include <stdint.h>
enum class TrafficLight : uint8_t
{
Red = 1,
Yellow,
Green
};
TrafficLight light = TrafficLight::Green;
enum class TemperatureScale : int16_t
{
Celsius,
Fahrenheit,
Kelvin
};
TemperatureScale scale = TemperatureScale::Celsius;
- 在上述两个例子中
TrafficLight
枚举类的底层类型是uint8_t
,这意味着枚举成员将使用一个字节的无符号整数来存储,这适用于那些只需要少量值的枚举,例如此处的交通信号灯状态TemperatureScale
枚举类的底层类型是int16_t
,它是一个16位的有符号整数,这个选择是因为温度尺度的值可能不需要太大的数值范围,但需要有符号整数来表示正负温度
四、优劣对比
4.1 enum class
的优势
4.1.1 减少命名污染
- 解决了传统枚举中作用域泄露的问题,在其他地方使用枚举中的变量就要声明命名空间
- 如下图所示,在某个包含了怪物类型枚举以及各怪物类的脚本内,使用
switch
语句对传统枚举类型进行比较,而EnemyType
枚举内的枚举常量的名称与怪物的类名冲突了,导致我无法正常实例化怪物对象,在我将enum
改为enum class
后这个问题就解决了
- 由于是传统的枚举,上图中的
case
后面的枚举常量不用加EnemyType::
也是合法的,这也是导致编译器无法分辨枚举常量和类名的原因 - 而
enum class
定义的枚举常量只能通过作用域解析运算符EnumName::
进行访问
4.1.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
#include <iostream>
int Add(int _num1, int _num2)
{
return _num1 + _num2;
};
int main()
{
//传统枚举
enum Color
{
Black,
White,
Red
};
//取出一个枚举常量
Color _test = Red;
//发生从Color到double的隐式转换
if(_test < 6.5)
{
//发生从Color到int的隐式转换
std::cout << Add(_test, 3) << "\n";
//输出结果为:5
}
}
4.1.3 可以前置声明
- 枚举类
enum class
可以被前置声明,即型别名字可以比其中的枚举量先声明
- 之所以可以前置声明,是因为
enum class
的底层类型是已知的(默认int
),即便可以被修改,但不论是哪一种,编译器都能提前知道这个枚举量的尺寸 - C++98的
enum
的底层类型是“实现定义”的,即编译器可以自行决定如何存储枚举值,一般会被映射为某种整数类型(通常是int
),不同的编译器可能有不同的选择,而类型不确定就无法分配内存,所以无法前置声明
4.2 enum
的优势
- 传统的
enum
并非被完全取代了,在某些情况下其仍具有优势,即当我们需要引用C++11中的std::tuple
元组的某个元素时
1
2
3
4
5
6
//元组的三个元素的含义分别是:名字、邮件、声望值
typedef std::tuple<std::string, std::string, std::size_t> UserInfo
UserInfo uInfo;
//取用域1的值
auto val = std::get<1>(uInfo);
- 在上述代码中我们要取
tuple
中的第二个值,但是如果第一次接触这段代码,很难知道<1>
到底是什么意思,而使用不限范围的枚举型别enum
和域序数关联就可以消除这种问题
1
2
3
4
5
6
7
8
9
10
11
12
typedef std::tuple<std::string, std::string, std::size_t> UserInfo
eunm UserInfoFields
{
uiName,
uiEmail,
uiReputation
};
UserInfo uInfo;
//一目了然,要获取邮件
auto val = std::get<uiEmail>(uInfo);
- 以上代码能够运行的原理就是
enum
可以隐式转换,而enum class
不接受隐式转换,必须要使用static_cast
进行强转才能实现上面的等效代码,代码会变得很啰嗦(当然,这种啰嗦的写法也确实能避免由于不限定作用域而带来的技术缺陷)
本文由作者按照 CC BY-NC-SA 4.0 进行授权