第24.2章:C++26 反射模型
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:]);
}
}
三个细节很重要:
-
std::define_static_array把nonstatic_data_members_of返回的 编译期 vector 提升到静态存储。展开语句需要一个元素能作为常量表达式使用的范围。 -
constexpr auto 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. 配方
本章每个反射算法都遵循同样的四步:
- 反射。从
^^T或^^member开始。 -
查询。向
std::meta询问成员、名字、类型、基类、枚举值、属性。 -
决定。用普通
if、循环和consteval函数。 这是正常的编译期代码,不是特殊 DSL。 - 拼接或生成。把结果变回表达式、类型,或新的聚合体。
语法不熟悉,控制流不是。如果你能写遍历 vector 的循环和对类型萃取做 if,
你就能写反射代码。唯一的新规则是:vector 装的是 std::meta::info 值,结果必须拼回文法。
9. 不能做什么
- 反射构造函数或析构函数,并把它当表达式拼接。构造函数和析构函数不是可拼接表达式。
- 反射局部实体并在作用域外使用。反射不延长生命周期。
- 对已经完整的类、带基类、虚函数或用户声明构造函数的类使用
define_aggregate。 - 查询不完整类型的成员。编译器必须知道成员列表;前置声明不够。
- 把
std::meta::info当运行时值序列化再加载。它是编译期句柄,不是可移植描述符。
下一页把模型用起来:枚举名字、字段访问器、JSON 和生成聚合体。代码更长,但每一行都是上面四步之一。