传统枚举的局限性

传统C++枚举(弱枚举)存在以下问题:类型不安全,枚举值隐式转换为整型,作用域污染,以及无法指定底层类型。这些问题可能导致代码可读性降低、维护困难,甚至引发难以察觉的运行时错误。

强枚举的优势

C++11引入的强枚举(enum class)解决了传统枚举的缺陷:

  • 类型安全:强枚举值不能隐式转换为整型,必须显式转换。
  • 作用域限定:枚举值属于枚举类的作用域,避免全局命名冲突。
  • 底层类型可指定:允许显式定义枚举的底层存储类型(如enum class Color : uint8_t)。
  • 可前置声明:强枚举支持前置声明,便于模块化设计。

工程实践中的应用

在工程代码中,强枚举更适合替代字符串或魔数(Magic Number),提升代码可维护性。例如,图像类型和分辨率应定义为强枚举而非字符串:

enum class ImageType { JPEG, PNG, BMP };
enum class Resolution { HD, FHD, UHD };

class Image {
    ImageType type;
    Resolution resolution;
    // ...
};

传统枚举的改进场景

尽管强枚举更安全,传统枚举在需要隐式转换或与旧代码兼容时仍有使用价值,但需谨慎处理类型冲突和范围检查问题。

// 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等非法值,仅靠注释无法保证设计的鲁棒性。

改进方案

通过枚举和常量替换松散的类型定义,可强制约束输入范围:

枚举定义图片类型

enum class ImageType { JPG, PNG, BMP, GIF };  // 严格限定支持的格式

尺寸参数使用常量约束

struct Resolution { 
    int width;  // 必须为正整数
    int height; // 通过构造函数/方法校验
};

改造后的Image类

class Image {
private:
    ImageType type_;      // 强制使用枚举值
    Resolution resolution_; // 结构体封装校验逻辑
public:
    Image(ImageType type, Resolution res) 
        : type_(type), resolution_(res) {} // 初始化时已确保合法性
};

优势

  • 类型安全:编译期即可拦截非法参数(如ImageType::TXT)。
  • 可维护性:枚举值集中管理,新增类型无需修改多处逻辑。
  • 显式约束:通过Resolution结构体可添加运行时校验(如width > 0)。

此设计消除了字符串和魔术数字的滥用风险,符合面向对象设计原则。

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的弱枚举

请看几个问题。

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

Image tmpImage = new Image(ImageType::JPG, ImageResolution::HIGH_2K);

在C++98的弱枚举(unscoped enumeration)设计中,枚举成员直接暴露在外部作用域,无需通过枚举类型名限定访问。这种设计可能引发命名冲突和隐式类型转换问题。

以代码Image tmpImage = new Image(JPG, HIGH_K);为例,若当前命名空间存在多个包含HIGH_K成员的枚举类型(如ImageResolutionCameraResolution),编译器无法区分具体引用哪个枚举值。这种情况在使用第三方库时尤为危险,不同库可能定义了相同名称的枚举成员。

传统枚举会隐式转换为整型,可能导致跨作用域的非预期类型转换。例如:

enum CameraResolution { HIGH, HIGH_2K };  
enum ImageResolution { HIGH, HIGH_4K };  // 编译错误:HIGH重定义

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

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

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

// 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关键字。通过对弱枚举的分析,强枚举的功效就比较明显了。

一、限定可见性

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

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

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

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

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也对他进行了改进并使其适应这样的写法。

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

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

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

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

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

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

// 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
Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐