💡 一分钟速览:C++ 枚举类型详解:传统枚举的坑、C++11 enum class 强类型枚举、类内枚举,以及 C++26 反射(std::meta)如何一段代码解决枚举转字符串,告别宏与查找表。
最后更新:2026-10-13
C++ enum 枚举类型¶
关于作者:¶
个人公众号:

传统行为¶
枚举有如下问题:
- 作用域不受限,会容易引起命名冲突。例如下面无法编译通过的:
enum Color
{
RED,
BLUE
};
enum Feeling
{
EXCITED,
BLUE
};
int main()
{
Color a = BLUE; // error
Feeling b = EXCITED;
std::cout << a << ":" << b << std::endl;
return 0;
}
具体实现见:tradition_color.cpp
经典做法¶
解决作用域不受限带来的命名冲突问题的一个简单方法是,给枚举变量命名时加前缀,如上面例子改成 COLOR_BLUE 以及 FEELING_BLUE。
一般说来,为了一致性我们会把所有常量统一加上前缀。但是这样定义枚举变量的代码就显得累赘。C 程序中可能不得不这样做。不过 C++ 程序员恐怕都不喜欢这种方法。替代方案是命名空间:
这样之后就可以用 Color::Type c = Color::RED; 来定义新的枚举变量了。如果 using namespace Color 后,前缀还可以省去,使得代码简化。不过,因为命名空间是可以随后被扩充内容的,所以它提供的作用域封闭性不高。在大项目中,还是有可能不同人给不同的东西起同样的枚举类型名。
更“有效”的办法是用一个类或结构体来限定其作用域,例如:定义新变量的方法和上面命名空间的相同。不过这样就不用担心类在别处被修改内容。这里用结构体而非类,是因为本身希望这些常量可以公开访问。
具体实现见:classic_practice.cpp
C++11 的枚举类¶
上面的做法解决了第一个问题,但对于后两个仍无能为力。庆幸的是,C++11 标准中引入了“枚举类”(enum class),可以较好地解决上述问题。
- 新的enum的作用域不在是全局的
- 不能隐式转换成其他类型
/**
* @brief C++11的枚举类
* 下面等价于enum class Color2:int
*/
enum class Color2
{
RED=2,
YELLOW,
BLUE
};
Color2 c2 = Color2::RED;
cout << static_cast<int>(c2) << endl; //必须转!
- 可以指定用特定的类型来存储enum
enum class Color3:char; // 前向声明
// 定义
enum class Color3:char
{
RED='r',
BLUE
};
char c3 = static_cast<char>(Color3::RED);
具体实现见:classic_practice.cpp
类中的枚举类型¶
有时我们希望某些常量只在类中有效。 由于#define 定义的宏常量是全局的,不能达到目的,于是想到使用const 修饰数据成员来实现。而const 数据成员的确是存在的,但其含义却不是我们所期望的。
const 数据成员只在某个对象生存期内是常量,而对于整个类而言却是可变的,因为类可以创建多个对象,不同的对象其 const 数据成员的值可以不同。
不能在类声明中初始化 const 数据成员。以下用法是错误的,因为类的对象未被创建时,编译器不知道 SIZE 的值是什么。(c++11标准前)
正确应该在类的构造函数的初始化列表中进行:
class A
{
A(int size); // 构造函数
const int SIZE ;
};
A::A(int size) : SIZE(size) // 构造函数的定义
{
}
A a(100); // 对象 a 的 SIZE 值为 100
A b(200); // 对象 b 的 SIZE 值为 200
怎样才能建立在整个类中都恒定的常量呢?
别指望 const 数据成员了,应该用类中的枚举常量来实现。例如:
class Person{
public:
typedef enum {
BOY = 0,
GIRL
}SexType;
};
//访问的时候通过,Person::BOY或者Person::GIRL来进行访问。
枚举常量不会占用对象的存储空间,它们在编译时被全部求值。
枚举常量的缺点是:它的隐含数据类型是整数,其最大值有限,且不能表示浮点。
枚举转字符串:C++26 反射的正解¶
把枚举值变成它的名字,几乎是每个 C++ 项目都会撞上的一次需求,但在 C++26 之前标准库一直没有正解。民间的办法按出现顺序排:(1)X 宏,用预处理器把枚举列表收敛到一处;(2)手写查找表或模板特化,每加一个枚举就要多维护一张表;(3)magic_enum 这类第三方库,靠编译器技巧把名字"钓"出来,但对枚举数量和底层类型都有要求。
C++26 静态反射(提案 P2996,std::meta)终于把这件事变成几行真正的代码。一个函数在编译期枚举任意枚举类型的成员,并读出每个枚举值的名字:
#include <meta>
template <typename E>
requires std::is_enum_v<E>
std::string enum_to_string(E value) {
template for (constexpr auto e : std::meta::enumerators_of(^^E)) {
if (value == [:e:]) {
return std::meta::identifier_of(e);
}
}
throw std::invalid_argument("unknown enumerator");
}
从此一个函数就能覆盖全代码库的所有枚举。枚举值改名后字符串自动跟着变,对不上直接在编译期报错,而不是运行时输出一个错的标签。
实话实说:上面这段代码遵循的是已并入 C++26 草案的 P2996,早期编译器预览版(EDG、Clang explorer)里个别拼法还在变,上线前先看自己工具链的支持表。在那之前,查找表仍是最可移植的零依赖方案。
常见问题¶
C++ 里怎么把枚举转成字符串?¶
C++26 之前没有标准做法,只能在查找表、模板特化、X 宏、magic_enum 这类库里选。C++26 静态反射直接在编译期枚举成员读出名字,把上面几种方案的维护成本一次清零。
标准库有内置的枚举转字符串吗?¶
C++26 之前没有。流输出和 std::to_string 只会格式化底层整数,所以每个项目最后都自己写了个工具函数。
C++26 反射什么时候能用上?¶
C++26 是即将发布的标准,反射已进入草案,EDG 和 Clang explorer 已经能编译示例。生产代码请盯着编译器支持状态,查找表方案留作兜底。
相关主题¶
- C++ 纯虚函数与抽象类 | C++那些事
- C++ assert 断言 | C++那些事
- C++ 位域 | C++那些事
- C 语言实现 C++ 多态 | C++那些事
- C++ const 详解:常量、常量指针与指针常量的区别 | C++那些事
- C++ const 全家桶 | C++那些事
- C++ 解引用运算符 * | C++那些事
- C++ 初始化与赋值 | C++那些事
- C++ 多返回值写法 | C++那些事
- C++ mutable 关键字 | C++那些事