最近重构项目时,发现其中一个模块用到了反射,不过它并非 C++ 原生实现,而是基于 Protobuf 实现的。正好 C++26 已经正式引入静态反射,便花了几天时间研究了一番。
虽然以前也接触过反射的概念,但 C++ 原生支持反射还是头一回。乍一看,最让初次接触的人困惑的就是两个新语法:^^ 和 [: ... :]。
这两个符号初看确实有点奇怪,但理清整体思路后会发现,C++26 这套反射机制的核心逻辑并不复杂。
在正式介绍它们之前,先来看一个问题:为什么 C++ 需要反射?
假设有这样一个结构体:
struct User {
int id;
std::string name;
};
对于编译器来说,User 是一个类型,里面有 id 和 name 两个成员,id 是 int,name 是 std::string——这些信息它当然都知道。
毕竟编译器在做类型检查、重载解析以及生成代码时,本身就要依赖这些信息。
问题是,过去这些信息基本只有编译器自己能用,程序很难直接拿到。比如想写一段通用代码,获取 User 有哪些成员、成员分别叫什么、各自是什么类型,旧版的 C++ 并没有提供标准方法。
所以很多看起来很自然的需求,实现起来反而没那么自然。
比如我们想把上面这个结构体反射成 JSON:
{
"id": 1001,
"name": "雨乐"
}
过去想做到这一点,通常需要自己维护额外的元数据,或者借助宏、模板元编程以及外部工具。比如项目中使用的 Protobuf 就是类似的思路:.proto 文件经过 protoc 处理后,会生成相应的描述信息,程序可以通过 Descriptor、Reflection 等接口获取 message、field 等信息。
如今 C++26 支持静态反射,程序可以在编译期间获取关于自身的信息。简单来说,就是把代码中的类型、变量、成员等实体变成编译期可以处理的数据。
整个过程可以概括为:代码 -> 反射信息 -> 代码。
前半部分由 ^^ 完成,后半部分则是 [: ... :]。
阶段一:^^ 获取反射信息
先看一个最简单的例子:
#include <meta>
int global;
constexpr std::meta::info type_info = ^^int;
constexpr std::meta::info var_info = ^^global;
在这个例子中,^^int 表示获取 int 这个类型的反射信息,而 ^^global 获取的是变量 global 的反射信息。
std::meta::info 是一个不透明的值类型,用来表示一个程序实体。所谓“不透明”,就是外界并不知道它内部到底保存了什么——可能是某种索引,也可能是编译器内部的其他表示。使用的时候,可以简单把它理解成编译器返回给我们的一个描述符。
比如 ^^int 得到的是描述 int 的 std::meta::info;^^global 得到的是描述变量 global 的 std::meta::info。
阶段二:[: :] 拼接反射信息
第一阶段通过 ^^ 把程序中的实体变成了 std::meta::info,接下来的问题是反方向:怎么把反射信息重新变回代码。
C++26 提供的语法是 [: ... :],这个过程叫做 Splicing,也就是把反射信息重新拼接回代码。
还是来看一个最简单的例子:
#include <meta>
int main() {
constexpr auto r = ^^int;
typename[:r:] x = 42;
}
第一句:
constexpr auto r = ^^int;
前面已经讲过,^^int 会得到描述 int 的 std::meta::info。
接下来:
typename[:r:] x = 42;
[:r:] 会把 r 描述的类型重新放回代码,所以这句话实际上就相当于:
int x = 42;
甚至还可以直接写:
typename[:^^char:] c = '*';
这里先通过 ^^char 得到 char 的反射信息,再通过 [:^^char:] 把它重新作为类型放回代码,最终就相当于:
char c = '*';
到这里,前面提到的两个阶段就串起来了:
int -> ^^int -> std::meta::info -> [: ... :] -> int
也就是:代码 -> 反射信息 -> 代码。
这其实就是 C++26 静态反射最基础的模型。
^^ 负责获取程序实体的反射信息,把类型、变量、成员这些原本只有编译器掌握的信息,变成我们可以在编译期处理的 std::meta::info。[: ... :] 则完成相反的事情,把这些反射信息重新放回 C++ 代码。
刚开始看到 ^^ 和 [: ... :] 的时候,我也觉得这两个语法多少有点奇怪。但把它们放到整个过程里看,就很好理解了:
代码 -> ^^ -> std::meta::info -> [: ... :] -> 代码
当然,这只是 C++26 静态反射最基础的一部分。