C++26 反射:从模板元编程到编译期内省

C++ 模板元编程一直是"工程师对机器"的对抗赛。你用 std::enable_ifif constexpr、折叠表达式这些工具在编译期辗转腾挪,每多一层抽象就多一层模板参数、多一层 typename...

P2996(Reflection for C++26)试图终结这种状态。它给 C++ 引入一组真正的**编译期内省(introspection)和代码注入(injection)**机制,让你能在编译期遍历类的成员、获取名称/类型、生成新代码——而不再依赖宏、外部代码生成器、或 Boost.Hana 这种"用模板模拟反射"的黑科技。

背景:C++ 为什么需要反射

在 Java、C#、Python 这类语言里,反射是标准库的一部分:你能在运行时获取一个类型的所有字段、调用任意方法、甚至动态创建对象。C++ 一直缺少这个能力,因为两个核心约束:

  1. 零开销原则:不能为不用反射的人付出任何运行时成本
  2. 编译期模型:C++ 的类型信息主要在编译期可用,运行时大多已擦除

C++ 社区的回应是"用模板元编程模拟反射"——std::tuplestd::is_same_vstd::decay_t、type traits……这些本质上都是用模板在编译期"计算"出类型信息。但它们是碎片化的:你需要为每种具体的元操作写一个 trait,没有统一的 API 来遍历"某个类型的所有成员"。

P2996 的方案很直接:编译期反射,零运行时开销。所有的反射操作都在 consteval 上下文中执行,生成代码片段(splicer)注入到 AST 中。

核心机制:^^[::]

P2996 引入了两个核心语法:

  • ^^(reflection operator):把语法结构反射成 std::meta::info 类型的值
  • [::](splicer):把 std::meta::info “拼接"回语法结构中

最简单的例子:

1
2
3
constexpr auto r = ^^int;            // 反射类型 int
typename [:r:] x = 42;               // 等价于:int x = 42;
typename [:^^char:] c = '*';         // 等价于:char c = '*';

你看,^^int 产生的 r 是一个反射值(类型是 std::meta::info),它不代表 int 本身,而是关于 int 的元信息typename [:r:] 把这个元信息重新拼回一个类型声明——C++ 标准的术语叫 splicing

同一个反射值可以用于不同的上下文:

1
2
3
4
5
constexpr auto r = ^^int;

typename [:r:]           // 类型上下文 → int
sizeof([:r:])            // 表达式上下文 → sizeof(int)
typename [:^^char:]      // 也可以直接嵌套 ^^

为什么用单一的 std::meta::info 类型

一个常见疑问是:为什么不按语言元素分类定义反射类型?比如 std::meta::variablestd::meta::typestd::meta::function

论文明确指出这是故意的设计选择:如果把语言设计编码到类型系统中,会让未来的语言演化极其困难——C++11 扩展了 variable 的语义包含引用,如果当时已经固定了 std::meta::variable 类型,就会面临破坏性变更。单一 opaque 类型让委员会保留了对语言演化的所有自由度。

遍历类的成员

反射的真正威力在于遍历。以 std::meta::nonstatic_data_members_of(^^T) 为例,它返回一个 vector<meta::info>,包含类型 T 的所有非静态数据成员。配合 C++26 的另一个核心提案 P1306 Expansion Statements,你可以这样遍历:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
#include <meta>

template <typename T>
void print_all(T const& v) {
    template for (constexpr auto e : std::meta::nonstatic_data_members_of(^^T)) {
        std::println(".{} = {}", std::meta::identifier_of(e), v.[:e:]);
    }
}

struct Point { int x, y; double z; };

int main() {
    Point p{1, 2, 3.14};
    print_all(p);
    // 输出:
    // .x = 1
    // .y = 2
    // .z = 3.14
}

template for 是 P1306 引入的编译期展开循环:它的循环体在编译期被展开成 N 份,每一份用循环变量的具体值替换。上面的代码等价于编译期展开为:

1
2
3
std::println(".x = {}", v.x);
std::println(".y = {}", v.y);
std::println(".z = {}", v.z);

实现一个通用序列化

有了反射和 expansion statements,很多过去需要宏或外部工具的任务变得非常简单。比如 enum → string

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
template <typename E>
  requires std::is_enum_v<E>
constexpr std::string enum_to_string(E value) {
    template for (constexpr auto e : std::meta::enumerators_of(^^E)) {
        if (value == [:e:]) {
            return std::string(std::meta::identifier_of(e));
        }
    }
    return "<unknown>";
}

enum class Color { Red, Green, Blue };

static_assert(enum_to_string(Color::Red) == "Red");

再比如 struct → JSON

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
template <typename T>
std::string to_json(T const& v) {
    std::string result = "{";
    bool first = true;

    template for (constexpr auto e : std::meta::nonstatic_data_members_of(^^T)) {
        if (!first) result += ", ";
        first = false;

        result += "\"";
        result += std::meta::identifier_of(e);
        result += "\": ";

        if constexpr (std::is_arithmetic_v<decltype(v.[:e:])>) {
            result += std::to_string(v.[:e:]);
        } else if constexpr (std::is_same_v<decltype(v.[:e:]), std::string>) {
            result += "\"" + v.[:e:] + "\"";
        }
        // else ... 递归处理嵌套 struct
    }

    result += "}";
    return result;
}

这不需要任何宏、不需要外部代码生成器、也不需要 Boost——所有逻辑在编译期一次 resolve,运行时只是一个普通的函数调用,没有任何反射开销。

更激进的例子:编译期 struct 变换

反射不仅能读,还能——也就是代码注入(code injection)。你可以根据现有类型在编译期构造出新的类型:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
consteval std::meta::info make_point_type() {
    // 动态构造一个 struct 类型定义
    std::vector<std::meta::info> members;
    members.push_back(std::meta::data_member_spec(^^double, {.identifier = "x"}));
    members.push_back(std::meta::data_member_spec(^^double, {.identifier = "y"}));
    return std::meta::define_aggregate(^^Point, members);
}

// 在编译期注入新的类型定义
[::make_point_type():];

int main() {
    Point p{1.0, 2.0};  // Point 此时已存在
}

这叫 compile-time type generation,已在 EDG 和 Bloomberg Clang fork 中实现。

实现状态

目前有两个可用的编译器实现,都可以在 Compiler Explorer 上在线测试:

实现方编译器状态
EDG (Edison Design Group)EDG C++ Front End覆盖 P2996 大部分特性,可直接使用
BloombergClang fork (clang-p2996)开源实现,已支持模板/命名空间 splicer

论文 P2996R13(2025-06-20)是当前最新版。P1306(Expansion Statements)也已进入 CWG 审查阶段。两者组合使用可以覆盖绝大多数反射场景。

与已有元编程方案的对比

方案类型遍历成员名获取代码注入编译器支持
C++17 type traits无(需手写)所有编译器
C++20 requires + concepts部分(条件约束)所有编译器
Boost.Hana有(模板模拟)所有主流编译器
Clang __reflection 扩展部分有限Clang only
P2996EDG / Bloomberg Clang
P2996 + P1306有(含展开)EDG / Bloomberg Clang

今日可执行动作

  1. 在线体验:打开 Compiler Explorer,选择 “EDG (experimental reflection)” 编译器,复制上面的 enum_to_string 例子跑一遍
  2. 本地编译:如果你用 Clang,克隆 Bloomberg 的 clang-p2996,按 README 构建后测试
  3. 阅读原文:阅读 P2996R13 的第 3 节(Examples)和第 4 节(Proposed Features),里面有 17 个完整的可运行示例

参考

CC BY-NC 4.0
最后更新于 2026-05-25