文章

对比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后这个问题就解决了

传统枚举在switch内的名称冲突.png

  • 由于是传统的枚举,上图中的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
    }
}
  • 限定作用域的枚举型别不允许发生任何隐式转换(除非强制转换),我们套用上述的代码就会发现下面的这些报错 枚举类的类型转换限制.png

4.1.3 可以前置声明

  • 枚举类enum class可以被前置声明,即型别名字可以比其中的枚举量先声明

传统枚举无法前置声明.png

  • 之所以可以前置声明,是因为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 进行授权