C++ 枚举类型:特点、用法与注意事项

枚举(enum)本质就是一组"具名整型常量"的集合:用名字替代魔法数字,读起来省心,改起来也集中。C++ 这些年对枚举的升级很明确:从"像 int"的老枚举,演进到"独立类型"的 enum class,再到 C++20/23 补齐易用性细节。

先给你一张速查表,把心智负担降到最低。


一、两种枚举形态:你到底在用哪一种

形态 写法 常见标准 枚举符是否"漏"到外围作用域 能否隐式当整型用
"无作用域枚举" enum C / C++98 起
"有作用域枚举" enum class / enum struct C++11 起 不会 不会

使用建议:新代码默认选 enum class;除非你明确需要跟 C 接口、老代码或 bitmask 习惯对齐。


二、C 与 C++98/03:老式 enum("无作用域枚举")

你会得到什么

一个最典型的用法

enum Direction { North, East, South, West };

void foo() {
    Direction d = East;
    int x = d;      // OK:隐式转整型
    if (d == 2) { } // 能编译,但可读性很差
}

这阶段最常踩的坑


三、C++11:enum class、底层类型、前向声明(关键分水岭)

3.1 enum class:把枚举变成"真正的类型"

它解决的是两件事:不污染名字不乱当 int 用

enum class Color : std::uint8_t { Red, Green, Blue };

void bar() {
    Color c = Color::Red;
    // int i = c; // 错误:不会隐式变成 int
}

3.2 指定"底层类型":想要稳定布局就别省这几个字

两种枚举都能指定底层类型:

enum Old : short { A, B };
enum class New : unsigned char { X, Y };

这通常用在三类场景:

3.3 前向声明:只有"固定底层类型"的枚举才好前置

如果你想把枚举当成接口参数类型,先声明、后定义,C++11 给了路子,但前提是必须固定底层类型。

enum Forwarded : int;    // 前向声明
void take(Forwarded* p); // 指针/引用 OK

四、C++11 起:和模板/标准库一起用(实战常见)

4.1 取底层类型:std::underlying_type_t<E>

当你需要把 enum class 做成整数(比如序列化、日志、位运算工具函数),这玩意是基本功:

#include <type_traits>

template <class E>
constexpr auto to_int(E e) -> std::underlying_type_t<E> {
    return static_cast<std::underlying_type_t<E>>(e);
}

五、模板元编程:枚举当"编译期标签"怎么用

在元编程里,枚举常常不是给运行时 switch 用的,而是给编译器看的:用名字表达策略,用不同枚举值驱动特化、if constexpr、重载决议。比裸 0/1/2 好读,也比散落的全局 constexpr int 更不容易和别的常量撞车。

用法一:非类型模板参数(NTTP)

满足语言规则时,枚举类型可以直接做模板的非类型实参:每个枚举符对应一整份不同的类型Foo<Kind::A>Foo<Kind::B> 是不同类型)。

enum class Op { Add, Mul };

template <Op O>
struct Traits;

template <>
struct Traits<Op::Add> {
    static constexpr int apply(int a, int b) { return a + b; }
};

template <>
struct Traits<Op::Mul> {
    static constexpr int apply(int a, int b) { return a * b; }
};

constexpr int x = Traits<Op::Add>::apply(2, 3); // 5

爽点:分支在编译期定死,该路径零运行时判断。
痛点:枚举多加一个值,所有全特化/分支可能要一起补;漏了往往直接编不过,这有时是好事,单有时也是维护成本。

用法二:std::integral_constant 打包成"类型"

(枚举类型, 某个枚举符) 变成一个类型,方便塞进 conditional_t、标签分发那套接口里。

#include <type_traits>

enum class Lane { Fast, Slow };

template <Lane L>
using LaneTag = std::integral_constant<Lane, L>;

// 别处:LaneTag<Lane::Fast>::value 即 Lane::Fast

用法三:if constexpr 按枚举分支

函数模板里按枚举拆枝,不要的代码不会进最终实例化(和运行时 if 不是一回事)。

enum class Mode { Sync, Async };

template <Mode M>
void run() {
    if constexpr (M == Mode::Sync) {
        // 只有 M==Sync 的实例里会保留这段
    } else {
        // 只有 M==Async
    }
}

好处(为啥元编程爱用枚举)

风险(别装不知道)

元编程里把枚举当 "带名字的编译期常量标签" 用很顺手;一旦枚举变成频繁膨胀的开放集合,这种写法维护量会上去,要早做拆分或代码生成。


六、C++17:更一致的初始化体验 + std::byte 这种典型用法

6.1 列表初始化:别让裸整数混进来

enum class 来说,{} 初始化很直观:只能用枚举符,不能塞个 1 进去糊弄。

enum class Hue { Red, Blue };

Hue h{Hue::Red}; // OK
// Hue bad{1};   // 不行:裸 1 不是 Hue

6.2 关联知识:std::byte

C++17 的 std::byte 本质就是一个"底层类型为 unsigned charenum class",用途就是:你要的是"字节"这个概念,而不是 char 的算术语义


七、C++20:using enum,让代码少点前缀但不丢类型安全

你想要 enum class 的安全,但 Status::Ok 写多了也烦。C++20 的 using enum 就是干这个的:只把枚举符引进来,不把类型边界拆掉

enum class Status { Ok, Fail };

void report(Status s) {
    using enum Status;
    switch (s) {
        case Ok:   break;
        case Fail: break;
    }
}

注意两点就行:


八、C++23:std::to_underlying,官方给你一个更顺手的转换

以前你要写 static_castunderlying_type_t,现在可以直接:

#include <utility>

enum class E : unsigned { A = 1 };

unsigned u = std::to_underlying(E::A);

语义就是:"把枚举转成它的底层整型"。短、清楚、少出错。


九、类里再套一层 enum:一个常见写法

有时候你不希望全局多出一个 ConnectionState,但又希望调用方一眼看出"这个状态只属于某个类"。这时就会在 类体里 再定义一个 enum / enum class,写成 类名::枚举名::枚举符 这种形式。

典型长什么样

class Connection {
public:
    enum class State { Closed, Connecting, Open };

    State state() const { return _state; }

private:
    State _state = State::Closed;
};

// 外面用:Connection::State s = Connection::State::Open;

老代码里也常见 无作用域 的写法:enum State { ... };,外面是 Connection::Closed 这种(枚举符直接挂在类作用域上)。新代码更推荐 类内 enum class,少踩"和整型乱混"的坑。

什么时候值得这么干

好处(为啥有人爱这么写)

风险(别装没看见)

类内嵌套枚举适合"强归属、API 愿意和类绑在一起"的场景;一旦枚举变成多方共用的契约,就别硬塞在类里,单独一个小头往往更省心。


十、通用注意事项(版本无关,但是真正决定你会不会踩坑)

10.1 默认选 enum class,除非你有明确理由

10.2 做协议/序列化/跨库传递:务必固定底层类型

比如:enum class MsgType : std::uint32_t { ... };。这个习惯会救你很多次。

10.3 switch 覆盖不全:别指望编译器一定提醒

建议在工程里打开 -Wswitch(GCC/Clang)或 MSVC 对应告警。否则枚举新增一个值,switch 漏处理,你可能要靠线上事故才发现。

10.4 位标志(bitmask)要专门设计,别硬拧

enum class 默认不支持 | & ^ ~ 这些运算符;真要做 bitmask,建议:固定底层类型 + 自己提供 operator| 等(或用项目里的 bitflag 工具)。不要在业务代码里到处 static_cast 乱飞。


十一、版本速查(你只想知道"哪版有啥")

标准 你能用的枚举相关能力
C / C++98/03 "无作用域枚举"、隐式转整型、底层类型实现定义
C++11 enum class、可指定底层类型、(固定底层类型时)可前向声明;枚举可作 NTTP 驱动模板特化
C++17 初始化更一致;std::byte 代表性用法;if constexpr 按枚举做编译期分支
C++20 using enum
C++23 std::to_underlying

十二、最后一句话(选型建议)