第24.2章:C++26 反射模型

English

1. 两个域

C++26 反射恰好有两个域:

域 包含 如何进入
文法域 类型、名字、表达式、声明、语句、成员、枚举值、模板。你能在源码里写的一切。 写普通 C++。
反射域 std::meta::info 类型的值。一个值表示一个实体或表达式。 ^^ 把文法元素反射成值。

你用 ^^ 从文法进入反射。用拼接 [: ... :] 回来。中间是操作 std::meta::info 值的普通 consteval 代码。

type T  ──^^T──►  info{^T}  ──nonstatic_data_members_of──►  [info, info, ...]
                                                        │
                                                        ▼
field access  ◄──[:m:]──  info{m}  ◄──template for──────┘

2. 反射运算符 ^^

^^ 接受一个文法元素,产生一个反射值。它不是 ^(异或),也不是对值 的一元运算符。语法是:

constexpr auto r1 = ^^int;             // a type
constexpr auto r2 = ^^std::vector;     // a template
constexpr auto r3 = ^^std::vector<int>; // a specialization
constexpr auto r4 = ^^person::name;    // a non-static data member
constexpr auto r5 = ^^color::green;    // an enumerator
constexpr auto r6 = ^^std::printf;     // a function
constexpr auto r7 = ^^std;             // a namespace
constexpr auto r8 = std::meta::reflect_constant(42); // a value
constexpr auto r9 = ^^std::is_same_v<int, int>; // a variable template specialization

结果是 std::meta::info 类型的常量表达式。它是不透明的:不能索引、不能转换成指针、 不能当文本打印。你用 std::meta 函数提问。注意 ^^ 反射的是实体 (类型、名字、变量、成员、枚举值),不是任意字面量;常量值用 std::meta::reflect_constant 反射,如 r8。

std::meta::info 可以比较。同一实体的两个反射相等;不同实体的反射不相等。 默认构造的 info{} 是空反射,不是 void 的反射。

static_assert(^^int == ^^int);
static_assert(^^int != ^^long);
static_assert(std::meta::info{} == std::meta::info{});

这个类型刻意不透明。委员会不想把 meta::type、meta::variable、 meta::function 这一族类型固定进类型系统,因为那会把语言的实体分类冻结在类型里。 一个类型加谓词更容易扩展。

3. std::meta 命名空间

<meta> 提供 consteval 函数,接受反射并返回反射、值,或两者。 名字很长但很规则。最先用到的:

函数 回答
type_of(r) r 表示的实体或表达式的类型。
identifier_of(r) 声明的名字,类型是 std::string_view。
display_string_of(r) 人类可读的拼写,不一定是合法标识符。
nonstatic_data_members_of(T, ctx) 类的非静态数据成员,按声明顺序。
enumerators_of(E) 枚举的枚举值,按声明顺序。
bases_of(T, ctx) 类的直接基类。
members_of(T, ctx) 所有成员:数据、函数、类型、构造函数等。
is_enum_type(r)、is_class_type(r)、... 对反射的谓词。
dealias(r) 类型别名或命名空间别名背后的实体。
substitute(template, args) 用给定参数特化模板后的反射。
extract<T>(r) 常量、枚举值或对象反射所代表的值。
reflect_constant(v) 常量值 v 的反射。
data_member_spec(type, opts) 一个数据成员的描述,供 define_aggregate 注入。
define_aggregate(T, specs) 用给定成员补全不完整类 T。

如果你用过 <type_traits>,很多谓词看着眼熟: is_integral_type、is_same_type、is_base_of_type、 is_convertible_type。它们是同样的问题,只是表达在反射上而不是模板参数上。

4. 拼接:回到文法

拼接把反射变回源码。上下文决定需要哪种文法元素:

形式 产生 例子
[: r :] 表达式、名字或值,取决于上下文。 int x = [: r :];
typename[: r :] 类型。 typename[: r :] x = 42;
[: r :]::name 作用域限定名。 [: r :]::value
template[: r :]<T> 模板特化。 template[: r :]<int>::value
obj.[: m :] 成员访问。 p.[: member :] = 42;

提案里的第一个例子就是完整的来回:

constexpr auto r = ^^int;
typename[:r:] x = 42;          // int x = 42;
typename[:^^char:] c = '*';    // char c = '*';

拼接不是字符串替换。编译器检查反射表示的实体类型是否符合文法要求。在需要类型的地方拼接函数反射 是错误,不是文本替换。

成员访问拼接比名字查找更直接。它不执行普通成员查找或重载决议;它访问反射表示的那个成员。 这就是它能在调用点不知道成员名字的情况下访问成员的原因。

5. template for:编译期迭代

template for 是展开语句。它看起来像范围 for,但在翻译期间展开。 每次迭代有自己的 constexpr 变量。

template <typename T>
void print_members(T const &obj)
{
    constexpr auto ctx = std::meta::access_context::current();
    constexpr auto members = std::define_static_array(
        std::meta::nonstatic_data_members_of(^^T, ctx));

    template for (constexpr auto m : members)
    {
        std::print("{} = {}\n", std::meta::identifier_of(m), obj.[:m:]);
    }
}

三个细节很重要:

template for 也支持索引和多个范围:

template for (constexpr std::size_t i{}; auto m : members)
{
    std::print("{}: {}\n", i, std::meta::identifier_of(m));
}

6. 生成类型:define_aggregate

反射不仅能检查已有声明。它还能从一组成员描述补全一个不完整聚合体。最小的形式:

#include <meta>
#include <print>

struct point;

consteval
{
    std::meta::info members[]
    {
        std::meta::data_member_spec(^^int, {.name = "x"}),
        std::meta::data_member_spec(^^int, {.name = "y"})
    };

    std::meta::define_aggregate(^^point, members);
}

int main()
{
    point p{.x = 3, .y = 4};
    std::println("({}, {})", p.x, p.y);
}

point 声明为不完整。那个 consteval 块在翻译期间运行,构建两个成员描述, 注入一个定义。从此以后,point 是一个有两个 int 成员的普通聚合体。

成员列表可以计算。一个常见模式是“结构体的数组版”:

#include <array>
#include <meta>
#include <vector>

template <typename T, std::size_t N>
struct struct_of_arrays
{
    struct impl;

    consteval
    {
        auto ctx = std::meta::access_context::current();
        std::vector<std::meta::info> specs;

        template for (constexpr auto m :
                      std::define_static_array(
                          std::meta::nonstatic_data_members_of(^^T, ctx)))
        {
            auto array_type = std::meta::substitute(
                ^^std::array,
                {std::meta::type_of(m), std::meta::reflect_constant(N)});
            specs.push_back(std::meta::data_member_spec(
                array_type, {.name = std::meta::identifier_of(m)}));
        }

        std::meta::define_aggregate(^^impl, specs);
    }

    impl data;
};

substitute 构建 std::array<MemberType, N> 的反射; data_member_spec 用原成员的名字命名它;define_aggregate 补全 impl。成员列表使用 std::vector<std::meta::info>; std::meta 查询本身返回同一类型,所以例子保持使用标准容器。

这也是独立环境的痛点。std::vector 不是独立容器,而返回范围的元函数 (nonstatic_data_members_of、enumerators_of、 members_of、bases_of)返回的是 std::vector<std::meta::info>。P3295 提案为独立环境增加 constexpr 容器, 让这些函数可用;在你的标准库提供它之前,这些函数只能在宿主环境使用。核心语言 (^^、拼接、std::meta::info)对独立环境友好;需要检查的是这些 返回容器的库函数。

这类代码过去需要外部生成器。限制是真实的:生成的聚合体是简单聚合体,不是带构造函数、 虚函数或访问说明符的任意类。

7. 访问上下文与取值

成员查询接受 std::meta::access_context。它说明查询从哪里发出,因为访问控制是语言的一部分:

constexpr auto here = std::meta::access_context::current();
constexpr auto all  = std::meta::access_context::unchecked();

current() 用当前函数或类作为访问上下文。这是安全默认:反射看不到当前代码不能命名的 私有成员。unchecked() 绕过访问检查,应该只留给确实需要布局的工具,比如调试器, 或者类自身实现里的二进制序列化器。

要从反射里取出值,用 extract<T>:

enum class color { red, green, blue };

constexpr auto e = std::meta::enumerators_of(^^color)[1];
static_assert(std::meta::extract<color>(e) == color::green);

extract 也是把常量反射变成模板参数或 switch 分支所需的真实值的方式。 它是 reflect_constant 的逆操作。

8. 配方

本章每个反射算法都遵循同样的四步:

  1. 反射。从 ^^T 或 ^^member 开始。
  2. 查询。向 std::meta 询问成员、名字、类型、基类、枚举值、属性。
  3. 决定。用普通 if、循环和 consteval 函数。 这是正常的编译期代码,不是特殊 DSL。
  4. 拼接或生成。把结果变回表达式、类型,或新的聚合体。

语法不熟悉,控制流不是。如果你能写遍历 vector 的循环和对类型萃取做 if, 你就能写反射代码。唯一的新规则是:vector 装的是 std::meta::info 值,结果必须拼回文法。

9. 不能做什么

下一页把模型用起来:枚举名字、字段访问器、JSON 和生成聚合体。代码更长,但每一行都是上面四步之一。