3870 字
19 分钟
C++ 26中的反射

反射进入 C++ 标准的讨论已持续多年,期间历经多版提案的修订,最终在 C++26 落地。本文讨论以下几个问题:反射的定义、C++26 采用的反射形式、具体语法、其引入的代价,以及它与其他语言反射机制的对比。

核心结论如下:

C++26 引入的是编译期静态反射:通过 ^^ 操作符将程序实体(类型、成员、枚举值等)转换为类型为 std::meta::info 的编译期值,再通过 [: :] 将其还原为代码。整个过程在编译期完成,不产生运行期开销,也不依赖额外的代码生成工具。

1. 标准化状态#

C++26 反射已完成标准化表决,进入工作草案,而非仍处于提案阶段。

核心提案为 P2996(Reflection for C++26),于 2025 年 6 月保加利亚索菲亚(Sofia)的 WG21 会议上正式投票纳入 C++26 工作草案。与之配套纳入的还有展开语句(expansion statements,template for,P1306)、注解(annotations)等一系列提案,共同构成可实际使用的反射工具集。

C++26 标准本身预计于 2026 年完成正式出版流程,因此当前处于特性已定稿、标准尚在出版的阶段。编译器支持情况如下:

  • GCC:自 16.1 起在主线中支持大部分 C++26 反射特性。
  • Clang:Bloomberg 维护了一个开源 fork(bloomberg/clang-p2996),是除 EDG 外的第二套实现。
  • EDG:提供一套持续推进的实现。
  • MSVC:截至 2026 年尚未公开支持。

Compiler Explorer(godbolt) 集成了 EDG 与实验版 Clang,可在线编写并验证反射代码。需要注意的是,当前各实现仍处于实验阶段,距离在生产环境中稳定使用尚有时间。

2. 反射的定义#

反射(reflection)指程序在自身运行或编译过程中查询并操作自身结构的能力

通常情况下,结构体包含哪些成员、各成员的名称与类型等信息仅为编译器所掌握,程序自身无法访问。反射机制将这些元信息(metadata)开放出来,使程序能够查询它们,并基于查询结果生成新的代码或行为。

一个典型场景是通用序列化函数的实现。在没有反射的情况下,每新增一个字段,都需要手动修改序列化代码:

struct User {
int id;
std::string name;
bool active;
};
// 手写:每次修改结构体,此处都需同步修改
std::string to_json(const User& u) {
return std::format(R"({{"id":{},"name":"{}","active":{}}})",
u.id, u.name, u.active);
}

借助反射,可以实现一个适用于任意结构体to_json,由函数自身查询其成员并逐一序列化。字段的增删不再需要修改函数本身。这正是反射要解决的核心问题:消除仅用于重复描述结构自身的样板代码

3. 静态反射与动态反射#

反射可分为两类,区别在于其发生的时机。

动态反射(runtime reflection)指在程序运行时查询和操作类型信息,Java、C#、Python、Go 均属此类。其优势在于灵活:可在运行时获取一个事先完全未知的类型(如插件、从配置文件读出的类名)并加以操作。代价是类型的元信息必须打包进可执行文件并始终携带,且查询本身存在运行期开销。

静态反射(compile-time reflection)指在编译期查询类型信息,编译器将反射结果直接展开为普通代码,运行时不再保留反射机制。C++26 采用的即为此种形式。

该选择符合 C++ 一贯的零开销抽象(zero-overhead abstraction)原则:未使用的特性不产生开销,使用的特性其效率不低于手写代码。其代价是丧失运行时的动态性,即无法在运行期获取一个编译期不存在的类型并对其反射。但对于 C++ 的主要应用场景(序列化、ORM、绑定生成、enum 转字符串等),待反射的类型在编译期通常均为已知,静态反射已能满足需求。

需要说明的是,C++ 此前已具备有限的动态反射能力,即 RTTI(运行时类型信息)的 typeiddynamic_cast。但其功能极为有限,仅能获取类型名称及在继承体系内进行向下转型,无法枚举成员,也无法获取成员名称,因此远非完整的反射机制。后文的对比中将再次提及。

4. 反射的用途与适用场景#

反射的主要用途包括:

  • 序列化与反序列化:JSON、二进制、配置文件等格式,可自动遍历成员,无需手写。这是反射最重要的应用。
  • enum 与字符串互转:常见于日志输出、调试和命令行参数解析,此前需依赖宏或冗长的 switch 实现。
  • 结构体与外部系统的映射:ORM(结构体与数据库表的映射)、RPC/IDL、GUI 数据绑定(如 Qt 的 QRangeModel 已采用)。
  • 生成绑定代码:为 Python、Lua 等脚本语言自动生成绑定。
  • 通用比较、哈希与打印:为任意聚合类型自动实现 operator==std::hash 及调试打印。
  • 游戏引擎:编辑器属性面板、序列化、网络同步、垃圾回收、脚本与可视化编程的对接等,均需在运行期或编辑期获知对象的成员结构,反射是其基础设施。

游戏引擎是反射需求的典型代表,而由于过去 C++ 缺乏语言级反射,主流引擎只能自行构建。Unreal Engine(UE)是最具代表性的例子:它通过一套宏(UCLASSUSTRUCTUENUMUPROPERTYUFUNCTIONGENERATED_BODY 等)标注需要反射的类型与成员,再由专门的 Unreal Header Tool(UHT)在正式编译前扫描这些头文件,生成 .generated.h 等包含反射元数据的代码。这套元数据支撑了 UE 的编辑器细节面板、对象序列化、网络复制(replication)、垃圾回收以及与蓝图(Blueprint)可视化脚本的互通。

这本质上是在标准 C++ 之上叠加了一层独立的代码生成与预处理流程——常被称为对 C++ 的”魔改”,因为这些宏并非标准语法,必须依赖 UHT 这一外部工具。C++26 的语言级反射理论上可以承担其中相当一部分职责,但游戏引擎的反射往往还需要运行期动态查询的能力(如运行时按名字访问属性),而 C++26 是纯编译期反射,因此短期内更可能是补充而非完全替代既有方案。

适用性的判断标准为:当代码的主要内容仅是重复列举结构体的字段名时,即适合采用反射;反之,若处理逻辑与具体业务相关、各字段的处理方式各不相同,则反射的作用有限,强行使用反而增加复杂度。

5. 反射的代价#

反射的代价可从以下几个维度分析。

运行期性能:基本为零。由于采用静态反射,所有查询均在编译期完成,生成的是与手写代码等价的普通代码。这与 Java、C# 的运行时反射存在本质区别,后者每次反射调用都伴随运行时查表开销。

是否生成额外文件:不会。这是相对于现有方案的主要优势之一。Qt 的 moc、Protocol Buffers 的 protoc 本质上都是额外的代码生成器,需要在构建系统中引入独立工具,先扫描源码生成中间 .cpp 文件,再统一编译。C++26 反射为语言内建机制,无需任何外部工具,也不产生中间文件,构建流程更为简洁。

编译时间:这是反射的主要代价。反射基于 consteval(编译期求值),大量使用会增加编译器的工作量,复杂的反射逻辑可能显著延长编译时间。其本质是将原本处于运行期或代码生成期的成本转移至编译期。对于编译本已较慢的大型 C++ 项目,这一点需要权衡。

二进制体积:默认不增加。仅当主动使用反射生成运行期所需的数据(如生成枚举名称表)时,体积才会相应增长,而这部分与手写同样的表所占体积一致,不存在额外开销。

学习与维护成本:新语法(^^[: :]template for)存在学习曲线,且元编程代码本身较普通代码更难阅读和调试。这是一项隐性成本。

6. 语法与 API#

C++26 反射主要由两个新操作符和 <meta> 标准库组成。

6.1 反射操作符 ^^#

^^(读作 caret-caret)为反射操作符,其作用是将程序实体转换为反射值,类型为 std::meta::info

#include <meta>
struct Point { int x; int y; };
constexpr std::meta::info r = ^^Point; // 反射一个类型
constexpr std::meta::info rx = ^^Point::x; // 反射一个成员

std::meta::info 是一个不透明的编译期类型,其内容无法直接访问,只能通过 std::meta:: 命名空间中的一系列 consteval 函数进行查询。由于它仅在编译期存在,必须以 constexprconsteval 承载。

6.2 拼接操作符 [: :]#

^^ 将实体转换为值,[: :](splicer)则执行相反操作:将反射值还原为代码并作为实体使用。

constexpr std::meta::info t = ^^int;
typename[:t:] n = 42; // 等价于 int n = 42;
Point p{24, 42};
constexpr auto ctx = std::meta::access_context::current();
constexpr auto member = std::meta::nonstatic_data_members_of(^^Point, ctx)[1];
std::cout << p.[:member:]; // 等价于 p.y,输出 42

二者的关系可概括为:^^ 将代码转换为数据,[: :] 将数据还原为代码。反射元编程的核心即在这两种操作之间转换。

6.3 查询 API 与 template for#

常用的查询函数均位于 std::meta:: 命名空间且均为 consteval

  • members_of(refl, ctx)nonstatic_data_members_of(refl, ctx):返回成员列表(一组 info)。需传入 access_context 参数,用于指定访问权限的视角。
  • enumerators_of(refl):返回枚举类型的全部枚举值。
  • identifier_of(refl):返回名称(std::string_view)。
  • type_of(refl):返回成员或实体的类型(仍为 info,可再次 splice)。

为遍历这些列表,C++26 引入了展开语句(expansion statement)template for,它在编译期将循环体针对列表中的每个元素各展开一次。

据此可将前述 to_json 的设想实现如下:

#include <meta>
#include <string>
#include <format>
template <typename T>
std::string to_json(const T& obj) {
std::string result = "{";
constexpr auto ctx = std::meta::access_context::current();
bool first = true;
template for (constexpr auto member :
std::meta::nonstatic_data_members_of(^^T, ctx)) {
if (!first) result += ",";
first = false;
result += std::format(R"("{}":)", std::meta::identifier_of(member));
result += std::format("{}", obj.[:member:]); // 取出该成员的值
}
result += "}";
return result;
}

此时无论 User 增加多少字段,to_json 均无需修改。template for 在编译期对每个成员展开循环体,identifier_of 提供字段名,obj.[:member:] 取出字段值,全部在编译期完成。

6.4 示例:enum 转字符串#

枚举值转字符串是一个常见需求,可清晰地体现反射的简洁性:

enum class Color { Red, Green, Blue };
template <typename E>
constexpr std::string_view enum_to_string(E value) {
template for (constexpr auto e : std::meta::enumerators_of(^^E)) {
if (value == [:e:]) {
return std::meta::identifier_of(e);
}
}
return "<unknown>";
}
// enum_to_string(Color::Green) == "Green"

相较之下,传统做法或为手写 switch(每增加一个枚举值都需相应修改),或为采用 X-macro 等宏技巧(可读性与可调试性较差)。反射版本则具备真正的通用性,且全部在编译期确定。

注:上述语法以最终纳入标准的写法为准。在当前实验性编译器上,遍历有时仍需使用 [:expand(...):] 这类库层面的过渡写法,template for 也可能需要启用实验开关,具体取决于所用编译器版本。

7. 对比#

7.1 与 C++ 既有方案的对比#

在 C++26 之前,实现类似反射的能力需依赖多种替代方案,各自存在明显局限:

方案原理主要局限
RTTI(typeid/dynamic_cast语言内建的运行时类型信息仅能获取类型名并进行向下转型,无法枚举成员,存在运行期开销
宏 / X-macro以宏将字段列表复用至多处可读性、可调试性差,报错信息晦涩
模板元编程 / Boost.PFR借助结构化绑定等技巧推断聚合体成员仅适用于聚合类型,无法获取成员名称,写法晦涩
Qt moc / protoc 等代码生成器外部工具扫描源码生成代码需修改构建系统、产生中间文件、属于 C++ 之外的方言

C++26 反射可视为上述方案的统一替代:语言内建、可获取名称、可枚举成员、无运行期开销、不依赖外部工具。

7.2 与其他语言的对比#

语言反射类型特点
Java / C#运行时反射功能完善、生态成熟(java.lang.reflectSystem.Reflection),但存在运行期开销,元数据需打包进产物
Python运行时内省灵活性高(getattrdir__dict__),为动态语言原生支持,但全部为运行时成本
Go运行时反射标准库 reflect,序列化与 JSON 编解码均依赖于此,存在运行时开销,采用类型擦除式 API
Rust无内建反射derive 过程宏在编译期生成代码(如 serde),思路与 C++ 静态反射相近
C++26编译期静态反射无运行期开销、不依赖外部工具,但无法反射运行期才出现的类型

由此可分为两大类:Java、C#、Python、Go 采用运行时反射,灵活但伴随运行期成本;Rust 与 C++26 采用编译期方案,将成本前移至编译期,以换取运行期的零开销。

其中 Rust 的情况值得对照:它不提供语言级反射,而是通过 derive 宏在编译期生成代码,serde 即为该机制的代表。C++26 的静态反射在设计理念上与之相近,二者均为编译期生成、运行期零成本;区别在于 C++ 将该能力实现为语言内建的反射操作,而非宏。

8. 总结#

  • C++26 反射(P2996)已于 2025 年 6 月正式进入标准草案,标准预计 2026 年出版;GCC 16.1、Clang 的 Bloomberg fork、EDG 已可在 Compiler Explorer 上试用,MSVC 尚未支持。
  • 其形式为编译期静态反射^^ 将实体转换为 std::meta::info 值,[: :] 将值还原为代码,配合 std::meta:: 查询函数与 template for 展开语句完成元编程。
  • 主要优势为无运行期开销、不依赖外部代码生成器、语言内建;主要代价为编译时间增加及一定的学习与调试成本。
  • 典型应用场景为序列化、enum 转字符串、ORM、绑定生成等以重复列举字段名为主的样板代码。
  • 与其他语言相比,它与 Rust 的 derive 同属编译期方案,与 Java、C#、Python、Go 的运行时反射方向相反,即以丧失运行时动态性为代价,换取零开销与简洁的构建流程。

反射的引入填补了 C++ 长期以来在语言层面缺乏完整反射能力的空白。随着编译器支持的逐步完善,序列化、枚举名称表、绑定代码等以往需要手写或依赖外部工具的部分,将可由编译器在编译期生成。


参考资料:

C++ 26中的反射
https://blog.gzher.com/posts/cpp-reflection-in-cpp-26/
作者
中会
发布于
2026-06-20
许可协议
CC BY-NC-SA 4.0