漫谈C++——强枚举到底有多强

C++学习笔记

Posted by Felix Zhang on July 28, 2020

强枚举到底有多“强”

传统枚举类型以及其特性在开发上的弊端已逐渐显露,C++11标准对原有枚举类型进行了加强和补充,从而衍生出了”强枚举”。本文不仅旨在讲清楚强枚举是如何在弱枚举上进行改进,更希望阐明弱枚举的种种不足是如何对开发造成不利影响的;以及同时C++11”新标准”(早已不新)是如何对98的枚举进行补充的。

枚举无处不在

在工程代码当中,只要和数据打交道,基本上就离不开枚举类型——这是因为单纯的字符串类型在一份合格的工程代码中是不应该出现的,所有的字符串都应以枚举或常量的形式定义。举个例子,如果我们有一个图像类,其中有两个成员分别代表了该图像的类型和分辨率,那么如果写成以下形式,那就要再去看一下工程规范了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// Image.h
// 定义
class Image{
private:
    String imgType;
    int resolution;
public:
    Image();
    Image(String imgType, int resolution);
  
/**
 * 其他成员已省略...
**/
};

// Process.cpp
Image tmpImage = new Image("jpg" 1920 * 1080)

上边调用是危险的,准确的来说问题出现在Image类的定义处,没有对两个成员的初始化作强限制。例如第一个参数调用者可能将参数传为”JPG”,就意思表达来说是准确的,但是会给代码带来极大的不确定性;后者也可被传入诸如1080 * 1920之类的不符合要求的值,即使在备注上加上说明,也不能保证这是一个良好的设计。

以上所有的传参都应被常量或者枚举所替换,如对于图片的类型,我们可以设计一个枚举,从而存放可能需要的类型,而在类中成员,我们将其定义为枚举类型。

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
enum ImageType{
    JPG = 100,
    PNG = 200,
    TIFF = 300,
    SVG = 400
};

enum ImageResolution{
    HIGH = 1920 * 1080,
    HIGH_2K = 2560 * 1440,
    HIGH_4K = 4096 * 2160
};

class Image{
private:
    ImageType imgType;
    ImageResolution resolution;
public:
    Image();
    Image(String imgType, ImageResolution resolution);
  
/**
 * 其他成员已省略...
**/
};

C++98的弱枚举

请看几个问题。

首先,在上例中,传统枚举的写法允许你这样写:

1
Image tmpImage = new Image(ImageType::JPG ImageResolution::HIGH_2K)

但同时,他还允许你这样写:

1
Image tmpImage = new Image(JPG, HIGH_K);

在C++98枚举(弱枚举)中,枚举类型是不限定作用域的(unscoped enumeration),枚举中的成员可不加命名空间限定符随意使用,但是不限定作用域的做法总是充满危险的,就上例而言,我们无法保证在当前命名空间是否不存在与ImageResolution相对应的CameraResolution枚举类型,其同样包含有成员HIGH, HIGH_2K, HIGH_4K中的一个或多个,若真的包含,很可能在不知觉中被调用者混淆——更让人难过的是,调用者可能并没有料到如此。这种场景在使用第三方库的情况下更为严重。因为传统枚举总是默认可以被隐式地转化为int类型,在不知不觉中就进行了跨作用域的类型转换。

说到隐式转换,这就不得不让人提到另一个让人心塞的问题。对于int类型所能表达的最大范围是2147483647,如果我们在显示得给枚举成员赋值时超出这个值呢?如:

1
2
3
enum annoyType{
    BIG_INTEGER = 2147483648;    // 这里会发生什么,会报错吗?
}

很遗憾,不会报错,而且什么事都不会发生。你可能会问,会有人这么做吗?在真实的应用场景中,很难保证人们不会这么写:很大程度上是因为等号后边的表达式并不那么显而易见。C++ 98的枚举类型很弱,可以进行对整形和浮点型的隐式转换,这很容易造成滥用——而且你并没有办法限定枚举的基类型为int或其他,上述的不确定性在下边的例子更显而易见。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// Debug的为32位整数,LogLevel的基类型可能为int
enum LogLevel{
    Fatal,
    Error,
    Warning,
    Debug = 0xFFFFFFFF
};

// 枚举成员最大值为2, Color的基类型可能为char
enum TrafficLightColor{
    red,
    yellow,
    green
}

以上代码编译错误,因为编译器无法知道LogLevel所需要的内存大小。重点在于若编译器不知道枚举成员的取值范围,就不知道如何设置枚举类型的基类型。C++是强类型语言,规定在使用任何类型之前,需要看到这个类型的声明,并且可以根据此声明推断出该类型所占内存的大小。否则无法通过编译。由于C++98枚举类型的大小必须在看到它的定义后才能知道,因此无法实现前置声明。因此,弱枚举类型必须定义在头文件中——编译依赖由此产生。假设有两个文件都包含了business.h一个是business.cpp用来实现接口,另一个client.cpp用来使用接口。如果修改了LogLevel的定义,如增加一个成员等级为Info,就需要将business.cppclient.cpp都重新编译,如果能实现前置声明,就可以将LogLevel的定义放在business.cpp中,当LogLevel的定义改变时,只需重新编译business.cpp即可,提高编译效率。

强枚举“强”在哪

强枚举官方限定名称为[ScopedEnumerations],定义强枚举需要在enum关键字后面加上class或者struct关键字。通过对弱枚举的分析,强枚举的功效就比较明显了。

一、限定可见性

强枚举的枚举成员可见性仅限定在枚举类型内:

1
2
3
4
5
6
7
enum color {red, yellow, green};    //不限定作用域的枚举类型
enum stoplight {red, yellow, green};//错误:重复定义了枚举成员
enum class pepers {red, yellow, green}; //正确:枚举成员被隐藏了
color eyes = green; //正确
peppers p = green;  //错误:pepeers的枚举成员不在有效作用域中,
                    //green对应的是color::green,但显然类型错误。
peppers p2 = peppers::red;          //正确使用peppers的red

二、更强的类型

强枚举是更强的枚举类型,不支持隐式转换为其他类型,如果要转换类型,必须显示地使用强制类型转换static_cast

1
2
3
int i = color::red; //正确,不限定作用域的枚举类型的枚举成员隐式地转换成int
int j = peppers::red; //错误,限定作用域枚举类型不会进行隐式转换。
int x = static_cast<int>(peppers::red); //正确

需要指出的是,尽管强制类型转换可以完成这样的”壮举“,但这并不意味着C++鼓励你去这么做——恰恰相反,这种限制希望你把不同的类型区分开,所以除非在一些项目历史原因导致的不合理的设计一定要用到这样的写法,则尽量避免。如果需要进行枚举成员之间的某种逻辑上的大小比较,可以直接规定清楚每个枚举成员的值,再进行直接比较。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
enum class LogLevel{
    Fatal = 500,
    Error = 400,
    Warning = 300,
    Debug = 200,
    Info = 100
};

// This is OK
LogLevel logLevel1 = LogLevel::Fatal;
LogLevel logLevel2 = LogLevel::Info;
if(loglevel1 > logLevel2){
    // Do something...
}

三、限定基类型和前置声明

这两点其实并不属于强枚举的”专利“,即使使用弱枚举,C++11也对他进行了改进并使其适应这样的写法。

1
2
3
4
5
enum intValues : unsigned long long {
    charTyp = 255, shortTyp = 65535, intType = 2147483647,
    longTyp = 4294967295UL,
    long_longTyp = 18446744073709551615ULL
};

如果我们没指定枚举的潜在类型,在强枚举中成员类型默认为int,而对弱枚举而言,枚举成员不存在默认类型,只知道成员的潜在类型足够大,肯定能够容纳枚举值。

但是:一旦定义了某个枚举成员的基类型(包括强枚举的隐式限定),一旦某个枚举成员的值超出了该类型所能容纳的范围,则会引发程序错误。

指定enum潜在类型的好处在于:

可以控制不同实验环境中使用的类型,我们可以确保在一种实现环境中编译通过的程序所产生的代码与其他实现环境中产生的代码一致。 ——《C++ Primer》

同样的对于前置声明,以下写法将被接受:

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
// business.h

//前置声明,并隐式声明基类型为int
enum class LogLevel;

class Logger{
    // Some members
    
private:
    void logging(LogLevel level, String message);
};

// business.cpp

#include "business.h"

//枚举的实现
enum class LogLevel{
    // Implementation
};

// client.cpp

#include "business.h"

//调用
void someFunc(LogLevel level){
    // Invokation
}

总结

  • 弱枚举的成员可见性与定义它的枚举相同;强枚举的可见性仅为枚举体内。
  • 弱枚举可以隐式类型转换,但强枚举必须采用显式,并且不建议这么做。
  • C++98中,弱枚举不支持前置声明;C++11中强、弱枚举都支持。
  • C++98中,弱枚举不支持基类型定义,枚举类型;C++11中强、弱枚举都支持,且强枚举默认基类型为int