目录

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

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


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

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

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


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

你会得到什么

  • 名字直接暴露在外层:enum Color { Red, Green }; 写完之后,外面直接用 Red 就行,这就是”名字污染”的根源。
  • 能当整型用:可以参与算术、比较、当数组下标,甚至随手传给 %d。
  • 底层类型不固定:编译器只要能装下所有枚举符就行,可能是 int,也可能更小/更大(跨平台、跨编译器的 ABI 场景别赌运气)。

一个最典型的用法

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

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

这阶段最常踩的坑

  • “名字污染”:不同枚举里都想用 Ok/Fail、Red/Green,会直接撞名。
  • “别拿 sizeof(enum) 当协议字段”:老枚举的大小可能因实现而变,做二进制协议或文件格式很危险。

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

3.1 enum class:把枚举变成”真正的类型” 它解决的是两件事:不污染名字、不乱当 int 用。

  • 枚举符要写全名:Color::Red,不会把 Red 扔到全局。
  • 不允许隐式转整型:想转就显式 static_cast,代码意图清楚很多。
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 };

这通常用在三类场景:

  • “ABI 稳定”:跨库/跨编译单元传递时不想被实现细节坑。
  • “协议/序列化”:网络包、文件格式,需要固定字节宽度。
  • “硬件寄存器”:位宽明确,类型也更贴合。

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

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

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

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

4.1 取底层类型:std::underlying_type_t 当你需要把 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)

满足语言规则时,枚举类型可以直接做模板的非类型实参:每个枚举符对应一整份不同的类型(FooKind::A 和 FooKind::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
    }
}

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

  • 意图清楚:模板实参是 Op::Add 而不是 0。
  • enum class 不跟 int 乱转:当模板标签时边界更干净。
  • 和特化、约束(C++20 概念) 都很好拼:例如 template requires (O == Op::Add) 这类写法(按你编译器/标准按需选用)。

风险(别装不知道)

  • 枚举演进成本:每加一个枚举符,特化表、if constexpr 链都要想清楚;有团队用 static_assert 故意在 default 里炸掉,逼你补全分支。
  • 标准没有”反射式遍历所有枚举符”:想在编译期 for 每个枚举值 自动生成代码,语言本身不包办;要么手写映射表/宏,要么上第三方库(例如 magic_enum 一类),别误以为 enum class 自带反射。
  • 报错栈变长:模板实例化 + 枚举名,IDE 跳转要耐心一点。

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


六、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 char 的 enum 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;
    }
}

注意两点就行:

  • 只在小作用域用(函数内、很小的块内),别再把”名字污染”请回来。
  • 多个枚举都 using enum 时,撞名会非常痛苦,能不用就不用。

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

以前你要写 static_cast 或 underlying_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 想表达归属关系:公开接口里返回 Foo::Kind,读代码的人不用猜这个 Kind 是给谁用的。
  • 想少污染命名空间:枚举不飞到全局,只通过 类名:: 访问,和”在 namespace 里包一层”有点像,但更贴类型。

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

  • 语义打包:枚举和类在文档、头文件、心智模型里是一起的,维护时不容易”孤儿枚举”。
  • 名字自然带前缀:Connection::State 比全局的 ConnectionState 对有些人更顺眼(看团队习惯)。
  • 配合访问控制:枚举可以写在 private: 里,当 实现细节 用,外面根本看不到(只适合完全不需要在 API 里暴露枚举的场景)。

风险(别装没看见)

  • 头文件一抖,全量重编:枚举放在类所在的头里,改一个枚举符、加一项,所有 include 这个头的翻译单元都要重新编译;枚举特别大、变更又勤时,要考虑是否拆到头文件边界更干净的地方。
  • 别的模块也要用这个类型时:如果 B 模块的函数参数也要写 Connection::State,就会 反向依赖 带 Connection 整套声明的头,容易产生 include 链变长、循环依赖。更通用的”协议级枚举”有时更适合放在独立小头文件里。
  • 类内无作用域 enum 的老坑还在:枚举符会进 类作用域,Connection::Closed 和别的类的 Closed 仍可能让人晕;能选就选类内 enum class。
  • 模板类里的嵌套枚举:写法上一般没问题,但若和 依赖名、前向声明搅在一起,新手容易在 typename、特化场景里多绕几圈(属于”进阶坑”,知道有这回事就行)。

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


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

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

  • 想要”不污染作用域”、“不和整数混用”:enum class。
  • 必须兼容 C 接口/老代码、或你明确要它当整数用:再考虑老 enum。

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++11enum class、可指定底层类型、(固定底层类型时)可前向声明;枚举可作 NTTP 驱动模板特化
C++17初始化更一致;std::byte 代表性用法;if constexpr 按枚举做编译期分支
C++20using enum
C++23std::to_underlying

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

  • 你想要”类型安全 + 不污染作用域”:直接 enum class。
  • 你想要”稳定布局/协议字段”:enum class X : std::uint32_t 这种写法别偷懒。
  • 你只是不想写 Status::Ok:在小作用域里 using enum 就行,别把全局变成战场。
  • 枚举只跟某个类强相关、又愿意和该类 API 绑在一起:类内嵌套 enum class 很合适;变成多方共用的契约时,单独小头文件更稳。
  • 模板里要”编译期策略标签”:用 enum class + 特化或 if constexpr 很顺手;枚举会频繁膨胀时要想好维护方式(表驱动/代码生成/三方反射库)。