第24.1章:为什么需要反射

English

1. 编译器回答不了的问题

假设你有这个类型:

struct person
{
    std::string name;
    int age;
    double height;
};

你想把它打印成 JSON:

{"name":"Ada","age":36,"height":1.68}

在大多数语言里,这是一个库调用。在 C++26 之前,这是手工劳动。编译器知道成员名字、成员类型、 顺序和布局。程序却问不到其中任何一项。信息在翻译期间存在,却从语言的表达式文法里消失了。

常见的变通办法都把知识搬到别处:

变通办法 知识住在哪里 哪里会出错
手写序列化器 类型旁边的第二个函数 会漂移。加一个字段忘了改序列化器,bug 是静默的。
宏列表 展开进结构体和访问器的文本列表 仍然是第二份声明。它看不到类型、基类或注解。
外部代码生成器 解析头文件的构建步骤 又一个解析器、又一个构建依赖、又一份会过期的输出。
编译器专用内建 __builtin_... 或插件 不可移植、不标准,而且不是每个目标都有。
运行时类型信息 RTTI、调试信息、注册宏 运行时成本,没有成员名字,而且只适用于多态类型。

反射就是缺失的那个操作:程序向编译器询问它正在编译的程序。它不是运行时库, 也不是调试器。它是一个编译期查询,而查询的答案可以再变回代码。

2. 编译期,不是运行时

Java 和 C# 有运行时反射:对象携带类描述符,程序可以在运行时枚举字段、调用方法、创建实例。 C++26 反射不同。它发生在编译期间,在常量求值器里。结果要么是一个常量值,要么是生成的代码。 程序运行时,反射已经消失,除非你显式生成了表。

属性 运行时反射(Java/C#) C++26 反射
何时运行 运行时 翻译期间,在 consteval 里
能看到什么 已加载的类型和元数据 这个翻译单元里编译器能命名的任何东西
运行时成本 元数据、查找、分配 如果生成的代码是直接访问,则没有额外成本
能创建新类型吗 通常不能 能,通过 define_aggregate 创建简单聚合体
能看到私有成员吗 有足够权限时可以 只能通过显式的访问上下文

编译期限制是特性,不是缺陷。编译器已经拥有完整的类型信息;在编译期询问意味着答案可以被类型检查、 内联和优化。运行时没有字符串查找,二进制里也没有元数据块。

3. 反射与生成是同一个特性

C++26 反射实际上是两种能力绑在一起:

  1. 反射:把程序实体变成 std::meta::info 类型的值。这个值可以保存、 比较、放进 vector,并被 consteval 函数查询。
  2. 拼接与生成:把反射变回文法元素——类型、名字、表达式、成员访问——或者从一组 反射成员定义一个聚合体。

只有第一种能力,就是一个没有使用答案方式的编译期查询语言。只有第二种,就是一个类型更好的宏系统。 两者结合,JSON 例子才可能:反射成员,查询名字和类型,然后生成字段访问。

grammar  ──^^──►  reflection value  ──std::meta::...──►  new reflection value
   ▲                                                              │
   └──────────────────────  [: ... :]  ───────────────────────────┘

上面的箭头是反射运算符 ^^。下面的箭头是拼接(splice): [: r :] 把 r 表示的实体插回程序。中间是普通的 consteval 代码:循环、if、std::vector、函数。

4. 第一眼:枚举转字符串

最经典的动机例子是把枚举值打印成名字。反射之前,每个项目都有一个 switch、一张宏表, 或者一个解析枚举声明的第三方库。

enum class color { red, green, blue };

template <typename E>
    requires std::is_enum_v<E>
constexpr std::string_view enum_to_string(E value)
{
    template for (constexpr auto e :
                  std::define_static_array(std::meta::enumerators_of(^^E)))
    {
        if (value == [:e:])
        {
            return std::meta::identifier_of(e);
        }
    }
    return "<unnamed>";
}

static_assert(enum_to_string(color::green) == "green");

先不要担心语法。看这三个步骤:

  1. ^^E 反射枚举类型。std::meta::enumerators_of 返回一组反射, 每个枚举值一个。
  2. template for 在编译期展开循环。每个 e 是一个 constexpr 反射值。
  3. [:e:] 把枚举值拼回表达式,所以 value == [:e:] 是普通比较。 identifier_of(e) 给出拼写。

编译器为每个枚举值生成一个比较。没有表查找、没有字符串解析、没有注册。如果你加一个枚举值, 函数自动看到它。

5. 为什么模板和类型萃取不够

C++ 已经有编译期计算。模板是图灵完备的,<type_traits> 回答数百个问题。 为什么那不算反射?

类型萃取回答的是库作者预先想到的问题:is_integral_v<T>、 is_base_of_v<A, B>、tuple_size_v<T>。它回答不了 “T 的成员名字按声明顺序是什么?”,因为语言里没有表达这个答案的语法形式。 这个萃取必须返回一组字符串和类型,而语言没有一种类型能容纳异构的程序实体列表。

模板也不能从一个看起来像运行时的列表生成新类型。你可以用参数包特化模板,但不能写一个循环, 把 data_member_spec 推进 vector,然后从它定义一个结构体。这正是 std::meta::info 和 define_aggregate 增加的东西。

宏是文本的,不是语义的。它们看到 token,看不到类型。宏不能问一个成员是不是位域、基类是不是虚的、 枚举的底层类型是什么。它也不能遍历一个不是自己声明的类型的成员。

6. 反射适合做什么

动机用例都很无聊,而且到处都是:

共同点是一份已经包含信息的声明,和一份重复它的第二声明。反射删除第二份声明。 这就是全部价值主张。

7. 反射不是什么

8. 标准的状态

反射于 2025 年 6 月作为 P2996 进入 C++26,同时还有注解、consteval 块、展开语句和 <meta> 库的支撑论文。语言特性包括:

编译器支持仍然不均衡。GCC 16 有一个实现;Bloomberg 的 clang-p2996 分支是参考 Clang 实现;EDG 有一个;写作时 MSVC 没有公开实现。这一章的代码是 C++26,不是库模拟。 在生产构建里使用之前,先查编译器支持表。

本章其余部分先讲模型,再讲模式。语法一开始看起来奇怪,因为它是新的。心智模型很小: 用 ^^ 反射,用 std::meta 查询,用 [: :] 拼接, 用 define_aggregate 生成。