1. What is reflection?(什么是反射)
1.1 直观定义
**反射(Reflection)**是指:
程序在运行时(或编译期)能够“观察并操作自身结构”的能力
具体来说,程序可以:
- 知道某个类型的名字
- 枚举一个类型的成员变量 / 成员函数
- 知道函数的参数类型、返回类型
- 在运行时根据字符串或元信息调用函数、访问字段
一句话概括:
Reflection = Code → Metadata → Code
1.2 非反射 vs 反射
没有反射(传统 C++)
structPerson{intage;std::string name;};Person p;p.age=10;// 必须在编译期知道成员名你不能:
- 用字符串
"age"访问p.age - 枚举
Person的所有成员 - 写一个“通用序列化器”而不手写代码
有反射(概念层面)
for(autofield:reflect<Person>.fields()){print(field.name,field.get(p));}这类能力就是反射。
1.3 编译期反射 vs 运行时反射
| 类型 | 特点 |
|---|---|
| 编译期反射 | 在编译时获取结构信息,不引入运行时开销 |
| 运行时反射 | 元信息在运行时存在,可动态查询 |
C++20 本身不提供标准反射
但:
- 可以模拟运行时反射
- 可以为未来的编译期反射(std::meta, P1240)做铺垫
2. Why?(为什么需要反射)
2.1 工程上的根本动机
反射解决的是一个核心问题:
“如何让通用代码理解用户定义的类型?”
2.2 没有反射时的痛点
(1)大量样板代码(Boilerplate)
structPerson{intage;std::string name;voidserialize(Json&j)const{j["age"]=age;j["name"]=name;}};- 每个结构体都要手写
- 字段一多就容易出错
(2)通用算法失效
你想写一个函数:
template<typenameT>voidprint_object(constT&obj);没有反射,你根本不知道:
T有哪些字段- 字段叫什么
- 如何访问
2.3 反射能带来什么
(1)通用序列化 / 反序列化
to_json(obj);// 自动from_json<T>(j);// 自动(2)ORM / RPC / 自动绑定
register_rpc_methods<MyService>();(3)调试 / 日志 / 可视化
dump(obj);// 打印所有字段(4)脚本语言 / 配置系统
{"type":"Person","age":18,"name":"Alice"}2.4 数学角度的抽象(为什么“通用”)
可以把一个对象抽象成:
Object = ( n 1 , v 1 ) , ( n 2 , v 2 ) , … , ( n k , v k ) \text{Object} = {(n_1, v_1), (n_2, v_2), \dots, (n_k, v_k)}Object=(n1,v1),(n2,v2),…,(nk,vk)
其中:
- n i n_ini是字段名(string)
- v i v_ivi是字段值(any)
反射的本质:
把“类型”从一个不可观察的编译期实体
映射成一个可枚举的结构集合
3. Implementing runtime reflection(如何实现运行时反射)
C++20没有原生反射
所以我们实现的是:“约定式 / 模拟反射”
3.1 基本思想
核心策略:
把类型信息显式存储成数据结构
即:
Type → Metadata → Runtime Query3.2 一个最小的反射系统组成
(1)字段描述结构
structFieldInfo{std::string_view name;std::function<void*(void*)>get_ptr;};含义:
name:字段名get_ptr(obj):返回字段地址
(2)类型描述结构
structTypeInfo{std::string_view name;std::vector<FieldInfo>fields;};3.3 为类型“注册”反射信息
structPerson{intage;std::string name;};注册:
TypeInfo person_type{"Person",{{"age",[](void*obj){return&static_cast<Person*>(obj)->age;}},{"name",[](void*obj){return&static_cast<Person*>(obj)->name;}}}};关键点:
- 使用
void*+ lambda 做类型擦除 - 成员访问被包装成函数对象
3.4 使用反射
Person p{18,"Alice"};for(auto&field:person_type.fields){std::cout<<field.name<<" = ";if(field.name=="age"){std::cout<<*static_cast<int*>(field.get_ptr(&p));}}3.5 抽象层级提升(工程级)
真实项目中通常会:
- 使用宏减少样板代码
- 使用模板自动生成
TypeInfo - 使用
std::any / std::variant承载值 - 结合 RTTI / type_index
例如:
#defineREFLECT_FIELD(type,field)\{#field,[](void*obj){return&static_cast<type*>(obj)->field;}}3.6 运行时反射的代价
优点
- 灵活
- 易与脚本 / 配置系统结合
缺点
- 性能开销(间接调用)
- 类型安全弱(
void*) - 编译期错误变成运行时错误
3.7 与未来 C++ 编译期反射的关系
未来提案(如std::meta)目标是:
constexprautoinfo=reflexpr(Person);forconstexpr(automember:info.members()){// 编译期展开}这意味着:
Reflection → constexpr Zero Runtime Cost \text{Reflection} \xrightarrow{\text{constexpr}} \text{Zero Runtime Cost}ReflectionconstexprZero Runtime Cost
4. 总结(Summary)
4.1 一句话总结
反射让程序“理解自己”,从而写出真正通用的代码
4.2 在 C++20 中的现实结论
- 没有标准反射
- 可以用模板 + 数据结构模拟运行时反射
- 需要权衡性能、类型安全与工程复杂度
- 为未来 C++ 编译期反射做好设计准备
1. Reflection 的核心定义
1.1 一句话定义
反射(Reflection)就是:把“代码本身的信息”当作“数据”来使用。
换句话说:
Reflection = Metadata of code
1.2 什么叫 “Metadata of code”
**元数据(Metadata)**指的是:
描述代码结构的数据,而不是代码运行时产生的数据
区分一下:
| 层级 | 例子 |
|---|---|
| 普通数据 | int health = 100; |
| 元数据 | “这个类型叫Entity,它有一个int health成员” |
2. “What members do I have?” 的真正含义
2.1 问题本身
当我们问:
“What members do I have?”
本质是在问:
一个类型在结构上由哪些组成部分构成?
2.2 以Entity为例
structEntity{inthealth;std::string tag;voideat_burger();};从反射视角,我们关心的不是“如何用它”,而是:
- 类型名:
Entity - 数据成员:
health,类型是inttag,类型是std::string
- 成员函数:
eat_burger(),返回void,无参数
这些信息在普通 C++ 里:
- 编译器知道
- 程序运行时不知道
2.3 没有反射时的状态(关键)
在 C++20 中:
Entity e;e.health=10;你不能写出:
for(automember:e.members()){...}// 不存在原因是:
成员信息在编译完成后被“抹掉”了
3. 用数学抽象理解“成员枚举”
我们可以把一个类型T TT抽象为一个集合:
T = m 1 , m 2 , … , m n T = { m_1, m_2, \dots, m_n }T=m1,m2,…,mn
其中:
- m i m_imi表示一个成员(member)
- 成员可以是:
- 数据成员
- 成员函数
- 类型别名(概念上)
对Entity来说:
E n t i t y = health:int , tag:string , eat_burger():void Entity = { \text{health:int}, \\ \text{tag:string}, \\ \text{eat\_burger():void} }Entity=health:int,tag:string,eat_burger():void
反射要做的事情就是:
在程序中把这个集合T TT显式地暴露出来
4. Reflection 在这里到底“反”了什么
4.1 正常编程方向
Type definition → Use members你先写结构体,然后在代码里硬编码地使用成员名。
4.2 有反射的方向
Type definition → Metadata → Generic code反射允许你:
for(automember:reflect<Entity>.members()){print(member.name,member.type);}即:从类型反推出成员
5. 成员为什么必须是“元数据”
5.1 如果成员不是元数据会怎样?
假设你想写一个通用打印函数:
template<typenameT>voiddump(constT&obj);如果没有成员的元数据:
- 编译器无法展开
- 程序无法枚举
- 你只能为每个类型单独写版本
5.2 有反射时的理想模型
反射系统内部通常等价于:
TypeInfo ( T ) = ( name , MemberInfo ∗ i ∗ i = 1 n ) \text{TypeInfo}(T) = \left( \text{name},\\ {\text{MemberInfo}*i}*{i=1}^{n} \right)TypeInfo(T)=(name,MemberInfo∗i∗i=1n)
其中:
MemberInfo至少包含:- 名字
- 类型
- 访问方式
6. 数据成员 vs 成员函数(反射视角)
6.1 数据成员
inthealth;std::string tag;反射关心:
- 偏移(offset)
- 类型
- 名字
访问模型(抽象):
value = ∗ ( b a s e p t r + o f f s e t ) \text{value} = *(base_ptr + offset)value=∗(baseptr+offset)
6.2 成员函数
voideat_burger();反射关心:
- 函数名
- 参数列表
- 返回类型
- 可调用性
抽象成:
call ( o b j e c t , a r g s . . . ) \text{call}(object, args...)call(object,args...)
7. 为什么 C++ 默认不给反射?
这是理解反射最重要的一点之一。
7.1 设计哲学原因
C++ 的核心哲学:
“你不用的东西,不要为它付出代价”
而反射意味着:
- 保留类型信息
- 增加二进制体积
- 增加运行时间接层
7.2 所以 C++20 的现实是
- 编译器知道所有成员
- 程序拿不到这些信息
- 需要手工或宏“重新描述一遍结构”
8. 小总结(针对你这页内容)
核心要点回顾
- Reflection = code 的 metadata
- 问题“What members do I have?”
本质是:类型结构是否可被程序观察 - 对
Entity而言:- 成员变量 + 成员函数构成其结构
- 数学抽象:
- 类型是成员的集合
- 反射就是暴露这个集合
1. Why should I care about reflection?
一句话先给结论:
因为反射让“通用程序”成为可能,而序列化是第一个、也是最典型的例子。
2. Serialization:为什么序列化离不开反射
2.1 什么是 Serialization(序列化)
序列化就是把内存中的对象,转换成一种可存储 / 可传输的表示:
- Binary(二进制)
- JSON
- XML
- Protobuf …
抽象地说:
Object ⟶ Bytes / Text \text{Object} \longrightarrow \text{Bytes / Text}Object⟶Bytes / Text
2.2 没有反射时的序列化困境
假设你有:
structEntity{inthealth;std::string tag;};传统 C++ 写法:
voidto_json(json&j,constEntity&e){j["health"]=e.health;j["tag"]=e.tag;}问题在于:
- 每个类型都要手写
- 字段改名 / 新增字段 → 必须同步修改
- 类型数量 × 序列化格式 = 样板代码爆炸
3. 反射如何让序列化“通用化”
3.1 把“结构”变成数据
反射的关键能力是:
让程序在运行时知道:一个对象有哪些字段
也就是:
Object = ( n a m e i , v a l u e i , t y p e i ) \text{Object} = {(name_i,\ value_i,\ type_i)}Object=(namei,valuei,typei)
3.2 你给出的核心伪代码(逐行理解)
jsonserialize_struct(any any_value){json json;for(Field f:reflect_fields(any_value)){json[f.name]["value"]=f.value();json[f.name]["type"]=f.type;}returnjson;}这段代码在“反射世界”里意味着什么?
any any_value- 类型被擦除了
- 但反射系统能恢复结构信息
reflect_fields(any_value)- 返回:这个对象“有哪些字段”
- 这是反射提供的能力
f.name- 字段名(字符串)
f.value()- 字段当前的值(仍然是
any)
- 字段当前的值(仍然是
f.type- 字段的静态类型信息
重点:
这里完全没有写health、tag
这段代码对所有结构体都成立
- 字段的静态类型信息
4. 递归序列化:真正的威力
4.1 顶层分发逻辑
你给出的第二段代码本质是一个通用 dispatcher:
jsonvalue_to_json(any any_value){// Handle builtinsif(any_value.type()==the_type<int>())returnjson{any_value.value<int>()};if(any_value.type()==the_type<double>())returnjson{any_value.value<double>()};if(any_value.type()==the_type<std::string>())returnjson{any_value.value<std::string>()};// Recurse for classesif(any_value.is_class()){returnserialize_struct(any_value);}}4.2 这段代码解决的核心问题
如何把“任意类型”转换为 JSON?
数学化理解:
serialize : Any → JSON \text{serialize} : \text{Any} \rightarrow \text{JSON}serialize:Any→JSON
递归定义:
- 基础类型:直接映射
- 复合类型:拆字段 → 对每个字段再调用
serialize
4.3 递归模型(非常重要)
serialize ( x ) = { json ( x ) x ∈ Builtin name i : serialize ( x i ) x ∈ Struct \text{serialize}(x) = \begin{cases} \text{json}(x) & x \in \text{Builtin} \\ {\text{name}_i : \text{serialize}(x_i)} & x \in \text{Struct} \end{cases}serialize(x)={json(x)namei:serialize(xi)x∈Builtinx∈Struct
这个递归只有在“能枚举字段”时才可能存在
5. Binary / JSON 只是“表现层”的差异
5.1 有了反射,格式只是策略
Object ↓ (reflection) Fields ↓ (policy) Binary / JSON / XML / Protobuf反射解决的是:
“对象长什么样”
序列化格式只是:
“我要怎么写出来”
6. Why do I care even more?
6.1 Reflection = Extension to the type system
这是非常关键的一点。
反射不是工具库,而是“类型系统能力的延伸”
7. 几个你列出的真实应用场景
7.1 WPF / Automatic Bindings(自动绑定)
典型模式:
bind(ui.health_bar.value,entity.health);背后需要:
- 字段名
- 字段类型
- 变化通知
抽象为:
UIProperty ↔ FieldInfo \text{UIProperty} \leftrightarrow \text{FieldInfo}UIProperty↔FieldInfo
没有反射,就只能靠: - 宏
- 代码生成
- 手写 glue code
7.2 Language bindings(Python / Lua)
当你写:
entity.health=10绑定层必须知道:
Entity有health- 它的类型是什么
- 如何读 / 写
这本质是:
Dynamic Language → Reflection Static C++ \text{Dynamic Language} \xrightarrow{\text{Reflection}} \text{Static C++}Dynamic LanguageReflectionStatic C++
7.3 Content editors(内容编辑器)
比如游戏编辑器:
- 自动列出所有字段
- 自动生成 UI 控件
- 修改即生效
没有反射: - 每个类型一个编辑器
- 维护成本极高
7.4 Automatic change detection(自动变更检测)
反射可以支持:
- 字段快照
- 差异比较
- 自动标脏(dirty flag)
形式化:
Δ = diff ( Object ∗ t 1 , Object ∗ t 2 ) \Delta = \text{diff}(\text{Object}*{t_1}, \text{Object}*{t_2})Δ=diff(Object∗t1,Object∗t2)
前提依然是:
字段是可枚举的
8. 总结(把所有点串起来)
8.1 为什么你“应该关心”反射
- 序列化不再是“为每个类型写一份代码”
- 通用算法成为可能
- 类型系统从“编译期封闭”变成“运行期可查询”
8.2 一句话总结
反射让类型从“只属于编译器”,变成“也属于程序本身”
如果你愿意,下一步我可以:
- 用C++20 + constexpr + templates写一个最小可运行的 JSON 反射序列化 demo
- 或把这个模型映射到你熟悉的Rust / serde / derive,对比两种设计哲学
1. How do we get there?(我们如何走到“有反射”的世界)
一句话先定调:
C++ 没有内建反射,但我们可以用一整套“语言机制 + 工程模式”逼近反射能力。
2. Implement reflection(实现反射:目标是什么)
2.1 我们真正想实现的能力
反射系统至少要回答这些问题:
- 一个对象是什么类型?
- 这个类型有哪些成员?
- 每个成员:
- 名字是什么?
- 类型是什么?
- 如何访问 / 调用?
抽象为一个函数:
reflect : Type → Metadata \text{reflect} : \text{Type} \rightarrow \text{Metadata}reflect:Type→Metadata
其中:
Metadata = name , members , attributes , … \text{Metadata} = {\text{name},\ \text{members},\ \text{attributes},\ \dots}Metadata=name,members,attributes,…
2.2 在 C++ 中的现实限制
- 类型信息主要存在于编译器内部
- 运行时只保留最少的 RTTI
- 成员列表、字段名、参数名统统不可见
结论:
反射必须“显式构建”,而不是“自动获得”
3. Go over current techniques(现有实现反射的技术路线)
下面是 C++ 社区目前真实在用的几种路线。
3.1 RTTI(最弱但原生)
typeid(T).name();dynamic_cast<Base*>(ptr);能力:
- 类型身份(identity)
- 继承关系(有限)
不能做的: - 枚举成员
- 访问字段
- 调用任意函数
RTTI ≠ Reflection
3.2 手写 Metadata(最直接)
structFieldInfo{std::string_view name;Type type;size_t offset;};为每个类型手动注册:
TypeInfo entity_info{"Entity",{{"health",int_type,offsetof(Entity,health)},{"tag",string_type,offsetof(Entity,tag)}}};优点:
- 完全可控
- 运行时可用
缺点: - 重复劳动
- 易出错
3.3 宏(现实中最常见)
structEntity{REFLECT()inthealth;std::string tag;};宏展开后:
- 生成字段列表
- 生成
TypeInfo
优点: - 使用成本低
- 工程可行
缺点: - 宏不可调试
- 破坏语义清晰度
3.4 模板 + constexpr(现代 C++20 风格)
核心思想:
把“反射信息”变成编译期常量
template<typenameT>structreflect;template<>structreflect<Entity>{staticconstexprautofields=std::make_tuple(field<&Entity::health>("health"),field<&Entity::tag>("tag"));};数学抽象:
reflect < T > ∈ constexpr \text{reflect}<T> \in \text{constexpr}reflect<T>∈constexpr
优点:
- 零运行时开销
- 类型安全
缺点: - 需要显式列字段
- 写法偏复杂
3.5 外部代码生成(Codegen)
流程:
C++ Source ↓ (parser) Metadata ↓ (generator) C++ Reflection Code例子:
- protobuf
- Unreal Header Tool
- Qt MOC
优点: - 能拿到“真实 AST”
- 表达能力最强
缺点: - 构建系统复杂
- 工具链重
4. Modules(模块化是反射的基础设施)
Modules 不是反射,但它解决了反射的“工程前置条件”
4.1 传统头文件的问题
#include → 文本展开 → 编译器内部 AST问题:
- 无法稳定地提取结构信息
- 工具难以对接
- 宏污染严重
4.2 Modules 带来的改变
module Entity; export struct Entity { ... };Modules 的关键特性:
- 明确的接口 / 实现边界
- 稳定的语义模型
- 更适合被工具消费
4.3 与反射的关系
反射提案依赖于:
Well-defined AST + Stable Interfaces \text{Well-defined AST} + \text{Stable Interfaces}Well-defined AST+Stable Interfaces
而 Modules 正是:
让“类型结构”成为一等语言实体
5. Patterns and Tricks for runtime reflection
(运行时反射的工程模式与技巧)
下面是即使没有语言级反射,你也可以用的实战技巧。
5.1 Type Erasure(类型擦除)
structAny{void*data;Type type;};目的:
把“不同类型”统一成一种运行时表示
数学模型:
Any = ( Type , Value ) \text{Any} = (\text{Type},\ \text{Value})Any=(Type,Value)
5.2 Offset + Pointer Arithmetic(字段访问)
value=*(base_ptr+offset);即:
field_value = ∗ ( p + Δ ) \text{field\_value} = *(p + \Delta)field_value=∗(p+Δ)
这是所有运行时字段访问的基础。
5.3 Visitor / Dispatcher 模式
visit(value,[](auto&v){// 根据真实类型分发});用于:
- 序列化
- 打印
- 比较
5.4 注册表(Type Registry)
unordered_map<TypeId,TypeInfo>registry;作用:
- 类型 → 元数据
- 动态查找
- 插件系统
5.5 Attributes / Annotations(模拟)
FIELD(range=[0,100])inthealth;用于:
- 编辑器
- 校验
- UI 自动生成
6. 一条完整“走到反射”的路径
可以总结为:
C++ Type → Templates / Macros / Codegen Metadata → Runtime Registry Reflection API \text{C++ Type} \xrightarrow{\text{Templates / Macros / Codegen}} \text{Metadata} \xrightarrow{\text{Runtime Registry}} \text{Reflection API}C++ TypeTemplates / Macros / CodegenMetadataRuntime RegistryReflection API
7. 总结(这一页你真正要传达的)
7.1 关键结论
- C++ 反射不是“等语言给”
- 而是:
- 模板
- 宏
- Modules
- 工程模式
的组合拳
7.2 一句话收尾
反射不是某一个特性,而是一整套设计方法。
1. Implementing an RTTI runtime(实现 RTTI 运行时)
1.1 从“理想客户端 API”开始
你最开始给出的接口是:
jsonserialize_struct(any any_value){json json;for(Field f:reflect_fields(any_value)){json[f.name]["value"]=f.value();json[f.name]["type"]=f.type;}returnjson;}这段代码在“设计上”想表达什么?
它假设了三件事:
- 对象是类型擦除的
any any_value; - 可以从对象直接反射出字段
reflect_fields(any_value) - 字段知道如何从对象中取值
f.value()
这是一个理想化 API,但在 C++20 中并不现实。
2. Go from realistic client API(走向现实可行的 API)
2.1 为什么必须“降级”API?
因为在 C++ 中:
- 对象本身不携带字段表
- 成员访问必须通过类型信息
- 运行时反射一定需要一个中心化的元数据存储
因此我们必须显式引入两个概念: - AnyRef:类型擦除后的对象引用
- Type:运行时类型描述
3. AnyRef:运行时对象的最小表示
structAnyRef{void*value;constType*type;};3.1 AnyRef 的语义
AnyRef本质是一个有序对:
AnyRef = ( address , Type ) \text{AnyRef} = (\text{address},\ \text{Type})AnyRef=(address,Type)
value:对象的内存地址type:指向类型元数据的指针
这比std::any更底层、更可控
3.2 为什么不是std::any
std::any:
- 不暴露内部布局
- 无法枚举字段
- 只能做“类型判断 + 强转”
而反射需要:
类型 → 字段 → 偏移 / 访问器
4. 用 Type 驱动反射,而不是对象
4.1 现实版的序列化函数
jsonserialize_struct(AnyRef any_value){json json;for(constField&f:any_value.type->fields){json[f.name]["value"]=f.value(any_value);json[f.name]["type"]=f.type;}returnjson;}关键变化只有一个:
any_value.type->fields这意味着:
字段信息来自“类型”,而不是对象本身
5. Field:字段到底是什么?
5.1 Field 的抽象定义
structField{std::string_view name;constType*type;Value(*value)(AnyRef);};name:字段名type:字段类型value():从任意对象中取出该字段的值
5.2 field.value(any_value) 在做什么?
逻辑等价于:
autobase=static_cast<char*>(any_value.value);autofield_ptr=base+offset;returnAnyRef{field_ptr,field_type};数学抽象:
field_addr = object_addr + Δ \text{field\_addr} = \text{object\_addr} + \Deltafield_addr=object_addr+Δ
其中Δ \DeltaΔ是字段在对象中的偏移量。
6. Type:类型元数据的核心
6.1 Type 的结构
structType{std::string_view name;std::vector<Field>fields;};语义上:
Type = ( name , Field ∗ i ∗ i = 1 n ) \text{Type} = \left( \text{name},\\ {\text{Field}*i}*{i=1}^{n} \right)Type=(name,Field∗i∗i=1n)
6.2 为什么字段列表属于 Type
因为:
- 字段是类型级信息
- 同一个类型的所有对象字段布局一致
- 对象只需要携带一个
Type*
这使得:
Memory overhead per object = O ( 1 ) \text{Memory overhead per object} = O(1)Memory overhead per object=O(1)
7. Type Registry:类型注册表
7.1 为什么需要 Registry
你必须回答这个问题:
“给我一个对象地址,我怎么知道它的 Type 是什么?”
答案:
必须有一个全局或模块级的 Type Registry
7.2 Registry 的基本结构
unordered_map<TypeId,Type>registry;或概念化为:
Registry : TypeId → Type \text{Registry} : \text{TypeId} \rightarrow \text{Type}Registry:TypeId→Type
7.3 注册流程(构造阶段)
register_type<Entity>({"Entity",{{"health",int_type,make_field<&Entity::health>()},{"tag",string_type,make_field<&Entity::tag>()}}});这一步:
- 把编译期结构
- 转换成运行时元数据
8. Public API:反射系统对外暴露的“最小面”
你在最后一页标注了:
Public API:
.type->fieldsfield.value()
这是非常重要的设计点。
8.1 为什么 Public API 要这么小
- 易于稳定
- 易于优化
- 易于替换实现
客户端代码只依赖:
any_value.type->fields f.value(any_value)8.2 客户端与实现解耦
序列化代码:
jsonserialize_struct(AnyRef any_value)完全不知道:
- offset
- pointer arithmetic
- registry 实现
- 模板 / 宏
这正是一个成功的 RTTI runtime 的标志。
9. 把整个系统串起来
完整的数据流:
Object → wrap AnyRef → Type* Type → fields Field → value() AnyRef \text{Object} \xrightarrow{\text{wrap}} \text{AnyRef} \xrightarrow{\text{Type*}} \text{Type} \xrightarrow{\text{fields}} \text{Field} \xrightarrow{\text{value()}} \text{AnyRef}ObjectwrapAnyRefType*TypefieldsFieldvalue()AnyRef
10. 一句话总结
RTTI runtime 的本质,是把“类型结构”从编译器手里,搬到程序手里。
1. Implementing type registry(实现类型注册表)
1.1 为什么需要 Type Registry
反射运行时必须能回答一个问题:
“系统里有哪些类型?给我一个名字,我能拿到它的结构吗?”
因此需要一个全局的类型存储中心。
1.2 最小形式的 Type Registry
externstd::unordered_map<std::string,Type>type_registry;语义上,这是一个映射:
TypeRegistry : TypeName → Type \text{TypeRegistry} : \text{TypeName} \rightarrow \text{Type}TypeRegistry:TypeName→Type
- key:类型名(字符串)
- value:类型的完整元数据
1.3 类型注册 API
template<typenameT>voidregister_type(Field[],Method[]);这一步在程序初始化阶段完成:
- 把编译期类型
T - 转换成运行时
Type对象 - 放入 registry
2. Defining Type(定义 Type)
2.1 Type 的角色
structType{std::string name;std::vector<Field*>fields;std::vector<Method*>methods;};Type是一个类型在运行时的“完整身份证”。
2.2 Type 的数学抽象
Type = ( name , Field ∗ i ∗ i = 1 n , Method ∗ j ∗ j = 1 m ) \text{Type} = \left( \text{name},\ {\text{Field}*i}*{i=1}^{n},\ {\text{Method}*j}*{j=1}^{m} \right)Type=(name,Field∗i∗i=1n,Method∗j∗j=1m)
它回答三类问题:
- 我叫什么?
- 我有哪些字段?
- 我有哪些方法?
2.3 为什么Type持有Field* / Method*
原因有三点:
- 类型擦除
Field/Method是多态基类
- 异构集合
- 不同字段 / 方法的具体实现不同
- 运行时统一访问
Type只关心接口,不关心实现
3. Defining Field interface(字段反射接口)
这一页是整个运行时反射的关键设计点之一
3.1 Field 的目标
把“成员变量”变成一个可以运行时操作的对象
3.2 为什么需要“类型擦除”
成员变量的类型是无限多的:
inthealth;std::string tag;floatposition[3];你不可能写:
std::vector<Field<int>>...std::vector<Field<std::string>>...所以必须:
抹掉具体类型,只保留行为
4. 1st instinct:虚函数基类
你给出的设计是最自然、也是最经典的方案:
classField{public:virtual~Field()=default;virtualstd::string_viewname()=0;virtualconstType*type()=0;virtualAnyRefvalue(void*object)=0;};4.1 每个接口函数在干什么?
(1)name()
virtualstd::string_viewname()=0;- 返回字段名
- 用于:
- JSON key
- 编辑器 UI
- 日志
(2)type()
virtualconstType*type()=0;- 返回字段的类型元数据
- 注意:不是 C++ 类型,而是反射系统的 Type
数学表示:
Field → type() Type \text{Field} \xrightarrow{\text{type()}} \text{Type}Fieldtype()Type
(3)value(void* object)
virtualAnyRefvalue(void*object)=0;这是最重要的接口。
语义是:
给我一个对象地址,返回这个字段对应的值
抽象为:
value : ( Object ∗ , Field ) → AnyRef \text{value} : (\text{Object}^*, \text{Field}) \rightarrow \text{AnyRef}value:(Object∗,Field)→AnyRef
4.2 为什么参数是void*
因为:
Field不知道宿主对象的静态类型- 只能通过地址 + 事先保存的信息(offset / 成员指针)来访问
5. Defining Method interface(方法反射接口)
字段解决的是“数据”,
方法解决的是“行为”。
5.1 Method 的设计
classMethod{public:virtual~Method()=default;virtualstd::string_viewname()=0;virtualconstType*return_type()=0;virtualstd::span<constType>parameter_types()=0;virtualAnyRefinvoke(void*object,std::span<void*>args)=0;};5.2 Method 接口的语义分解
(1)name()
- 方法名
- 用于:
- 脚本绑定
- RPC
- UI 按钮
(2)return_type()
virtualconstType*return_type()=0;数学表示:
Method → return_type() Type \text{Method} \xrightarrow{\text{return\_type()}} \text{Type}Methodreturn_type()Type
(3)parameter_types()
virtualstd::span<constType>parameter_types()=0;- 返回参数类型列表
- 顺序即调用顺序
抽象为:
Method → ( Type 1 , … , Type n ) \text{Method} \rightarrow (\text{Type}_1, \dots, \text{Type}_n)Method→(Type1,…,Typen)
(4)invoke(...)
virtualAnyRefinvoke(void*object,std::span<void*>args)=0;语义是:
在运行时,用一组“无类型参数”调用该方法
数学模型:
invoke : ( Object ∗ , Args ∗ ) → AnyRef \text{invoke} : (\text{Object}^*, \text{Args}^*) \rightarrow \text{AnyRef}invoke:(Object∗,Args∗)→AnyRef
5.3 为什么args是void*
原因与字段一致:
- 参数类型已通过
parameter_types()描述 invoke只负责调度与调用- 类型检查由反射层或上层完成
6. Field / Method 是“类型系统的镜像”
可以这样理解:
| C++ 静态世界 | RTTI 运行时世界 |
|---|---|
| 成员变量 | Field |
| 成员函数 | Method |
struct/class | Type |
| 编译器 | Type Registry |
7. 设计代价与收益
7.1 代价
- 虚函数调用(间接调用)
- 内存占用
- 初始化顺序管理
7.2 收益
- 真正的运行时反射
- 动态语言 / 编辑器 / RPC 友好
- 客户端 API 极简、稳定
8. 一句话总结(这一页的核心)
Type Registry + Field / Method 接口 = 一个可运行的“迷你类型系统”。
如果你愿意,下一步我可以:
- 展示FieldImpl / MethodImpl 如何用模板生成
- 或分析不用虚函数、改用 function pointer / constexpr table 的替代设计
Gathering type data → Manual → Automatic(多种),并在最后给一个整体对照与结论。
1. Gathering type data
Gathering type data is the meat and potatoes of our reflection library
因为:
- Registry / Field / Method 只是“容器”
- 真正困难的是:如何获得这些信息
1.1 Type Registry 的角色回顾
Type registry: Contains type definitions本质映射:
TypeRegistry : C++ Type → Runtime Type \text{TypeRegistry} : \text{C++ Type} \rightarrow \text{Runtime Type}TypeRegistry:C++ Type→Runtime Type
1.2 对外 Public API(极其重要)
你反复强调的 API:
.type->fields field.value()这是刻意设计的结果:
- 客户端代码永远不关心
- 类型如何注册
- 字段如何生成
- 用的是宏 / 模板 / parser
1.3 注册入口函数
template<typenameT>voidregister_type(Field[],Method[]);这一行是编译期世界 → 运行时世界的“桥”。
2. Current techniques: Manual(手动方式)
2.1 代表:RTTR
structMyStruct{MyStruct(){};voidfunc(double){};intdata;};RTTR_REGISTRATION{registration::class_<MyStruct>("MyStruct").constructor<>().property("data",&MyStruct::data).method("func",&MyStruct::func);}2.2 这种方式在做什么?
本质上是:
“让用户把编译器知道的结构,再写一遍给程序”
数学化表达:
C++ AST → Human Metadata \text{C++ AST} \xrightarrow{\text{Human}} \text{Metadata}C++ ASTHumanMetadata
2.3 优点
- 稳定
- 可控
- 不依赖工具链
- 易于调试
2.4 致命问题
(1)重复定义
intdata;// 定义一次.property("data")// 再写一次→DRY 原则被破坏
(2)维护成本指数级增长
如果:
- 类型数量 =N NN
- 字段数量 =M MM
维护成本近似为:
O ( N × M ) O(N \times M)O(N×M)
3. Current techniques: Automatic(自动方式)
目标是:
让“类型数据的获取”不再依赖人工维护
4. Automatic #1:std::tuple_element技巧
4.1 代表库
boost::pfrmagic_get
4.2 原理
利用聚合初始化规则:
structS{inta;doubleb;};编译器允许:
auto[x,y]=s;于是可以推导:
S ≡ tuple < i n t , d o u b l e > S \equiv \text{tuple}<int, double>S≡tuple<int,double>
4.3 能做到什么
- 字段数量
- 字段类型
- 字段顺序
4.4 做不到什么(关键)
- 字段名
- 成员函数
- 私有成员
- 属性 / 注解
所以你总结得非常准确:
Not flexible enough
5. Automatic #2:Code Parser(代码解析器)
5.1 代表方案
- Qt:Meta Object Compiler (MOC)
- Unreal Engine:Unreal Header Tool (UHT)
5.2 思路
C++ Source ↓ (parser) AST ↓ (generator) Reflection code5.3 问题 1:C++ 语法极端复杂
- 模板
- 宏
- 条件编译
- 重载 / ADL
解析“完整 C++”几乎等于“写一个编译器”
5.4 问题 2:维护成本灾难
你原文说得非常直白:
Maintenance is a mess
原因:
- 标准更新频繁
- 各家编译器行为不同
- 用户代码风格多样
5.5 问题 3:构建系统深度绑定
必须处理:
- include 路径
- 宏定义
- 编译选项
- 平台差异
数学上可以理解为:
Parser ⊆ Build System \text{Parser} \subseteq \text{Build System}Parser⊆Build System
6. Automatic #3:使用编译器前端(LibClang)
6.1 思路升级
既然 C++ 太难解析,那我直接用编译器
6.2 工具
- LibClang
- ClangTooling
6.3 优点
- 语法完全正确
- AST 信息最完整
- 与标准同步
6.4 现实问题
(1)慢
- 全量 AST
- 模板实例化
(2)构建系统依然复杂
仍然需要:
- 编译参数
- 宏
- include 路径
7. 为什么 Automatic 方案仍然“不完美”
可以总结为一句话:
所有自动方案,本质都是“在语言之外模拟语言反射”
数学抽象:
Reflection ≈ Compiler AST Export \text{Reflection} \approx \text{Compiler AST Export}Reflection≈Compiler AST Export
但问题是:
- AST 不是稳定 API
- 编译器不是库
8. 对比总结表
| 方案 | 优点 | 缺点 |
|---|---|---|
| Manual | 简单、稳定 | 重复、易错 |
| tuple 技巧 | 无工具链 | 能力极弱 |
| 自写 parser | 理论最强 | 不现实 |
| LibClang | 信息完整 | 慢、复杂 |
9. 这一页真正想表达的结论
“收集类型数据”是反射最难的问题,而现有方案都只是权宜之计。
10. 一句话收尾
反射不是缺接口,而是缺“编译器到程序”的正式通道。
1. C++ compilation process(C++ 编译流程)
我们先把你给出的流程整理成一条标准管线:
Interfaces (.h) ↓ Sources (.cpp) ↓ Compiler (cl.exe / g++ / clang++) ↓ Intermediate (.obj / .o) ↓ Linker (link.exe / ld / lld) ↓ Executable (.exe)2. Headers mostly contain type info
(头文件主要承载“类型信息”)
2.1 这是一个设计事实,不是偶然
在 C++ 的工程实践中:
.h:struct / class- 成员变量
- 成员函数声明
- 模板定义
.cpp:- 函数实现
- 算法逻辑
- 私有细节
也就是说:
几乎所有“反射关心的信息”,都集中在头文件里
2.2 用反射视角重新理解头文件
传统理解:
.h是“为了让别的文件能编译”
反射视角:.h是类型系统的公开接口描述
可以抽象为:
Header ≈ Public Type Metadata \text{Header} \approx \text{Public Type Metadata}Header≈Public Type Metadata
3. 编译器在中间到底做了什么?
3.1 编译器的真实工作模型
Source (.cpp + included .h) ↓ Preprocess (#include / macro) ↓ Parse ↓ AST(抽象语法树) ↓ Semantic analysis ↓ IR / Object file关键点:
类型信息在 AST 阶段是最完整、最精确的
3.2 但 AST 的命运是?
- AST 只存在于编译器内部
- 生成
.obj / .o后:- 成员名消失
- 字段结构消失
- 只剩下偏移和符号
可以理解为:
AST → Compile Binary (Type-erased) \text{AST} \xrightarrow{\text{Compile}} \text{Binary (Type-erased)}ASTCompileBinary (Type-erased)
4. 为什么反射工具“盯上”头文件?
4.1 因为这里是唯一还没丢信息的地方
对比不同阶段的“信息密度”:
| 阶段 | 类型信息 |
|---|---|
.h | 完整 |
.cpp | 完整 |
| AST | 完整 |
.obj | 几乎没有 |
.exe | 几乎没有 |
| 结论: |
反射只能在“编译之前或期间”发生
5. Interfaces(.h)的两条技术路线
你给出的两条路径非常关键。
5.1 路线一:Parse source code
(解析源码)
.h ↓ Parse ↓ Metadata这就是:
- 自写 parser
- 或使用 LibClang
5.2 Precompile + LibClang pass
更现代的做法是:
.h ↓ Precompile (PCH) ↓ LibClang AST pass ↓ Reflection data优点:
- AST 真实
- 与编译器语义一致
缺点: - 慢
- 构建系统耦合极深
6. Sources(.cpp)的现实情况
6.1 为什么.cpp里“手动放反射代码”
你写了这一句:
Sources (.cpp) • Manually put in source
这在现实项目中非常常见,原因是:
.cpp是唯一可以安全放“副作用代码”的地方- 注册表初始化
- 静态对象构造
- 工厂注册
典型模式:
// entity.cppstaticboolregistered=[]{register_type<Entity>(...);returntrue;}();6.2 为什么不放在.h
因为:
.h会被多次 include- 容易 ODR 问题
- 静态初始化不可控
7. 从反射角度重新画一条“隐含流程”
真实的反射系统往往是这样:
.h ↓ (parse / inspect) Type metadata ↓ Generated / handwritten registration code ↓ .cpp ↓ Type Registry (runtime)数学化描述:
Compile-time Types → Extraction Metadata → Initialization Runtime Types \text{Compile-time Types} \xrightarrow{\text{Extraction}} \text{Metadata} \xrightarrow{\text{Initialization}} \text{Runtime Types}Compile-time TypesExtractionMetadataInitializationRuntime Types
8. 为什么这件事这么“别扭”
因为 C++ 的编译模型本质是:
“编译器拥有类型系统,程序不拥有”
而反射的需求是:
“程序也要拥有类型系统”
这是一个根本性的张力。
9. 这一页真正想表达的结论
9.1 技术结论
- 反射最关键的数据在
.h - 编译器在 AST 阶段掌握一切
- 二进制阶段已经太晚
9.2 工程结论
所有现有 C++ 反射方案,本质都是“在编译流程中偷偷插一刀”
10. 一句话收尾
C++ 反射的难点,不在“怎么存元数据”,而在“怎么把它从编译器里拿出来”。
一、一句话总览(先给直觉)
Modules 让“类型信息第一次成为正式编译产物”,
但并没有给你“在编译过程中使用这些信息的权力”。
二、“Modules are a tooling opportunity”是什么意思?
Gabriel dos Reis 这句话非常精准,但也非常容易被误读。
错误理解
Modules = 反射
Modules 自动解决 RTTI / Reflection
正确理解
Modules为工具提供了前所未有的机会
但并没有改变语言的反射能力
也就是说:
Modules = Better Compiler Artifact ≠ Reflection Feature \text{Modules} = \text{Better Compiler Artifact} \neq \text{Reflection Feature}Modules=Better Compiler Artifact=Reflection Feature
三、Modules 带来的真实变化
1⃣ 模块接口先被单独编译
Module interfaces .ixx / .cppm ↓ Compiler ↓ Binary Module Interface (BMI) .ifc / .pcm / .gcm这是Modules 与传统头文件的根本差异。
2⃣ 什么是 Binary Module Interface(BMI)?
BMI 不是.obj,也不是头文件缓存,而是:
- 已完成语义分析的结果
- 包含:
- 类型定义
- 成员
- 访问控制
- 模板关系
- 语义约束
可以近似理解为:
BMI ≈ 编译器内部 AST 的持久化版本 \text{BMI} \approx \text{编译器内部 AST 的持久化版本}BMI≈编译器内部AST的持久化版本
但注意:这是“编译器私有格式”
3⃣ 为什么说 “Fast! Work already happened”
在没有 Modules 时:
每个 .cpp ↓ 重复 parse 头文件 ↓ 重复构建 AST有 Modules 后:
Module interface → BMI(一次) .cpp import → 直接用所以:
- 类型分析只做一次
- 后续
.cpp零解析成本
四、完整编译流水线(没有反射)
你图中上半部分表达的是:
.ixx / .cppm ↓ Compiler ↓ BMI然后:
.cpp (import BMI) ↓ Compiler ↓ .obj / .o ↓ Linker ↓ .exe这里有一个非常重要的事实:
.cpp已经不再“拥有”类型定义,它只是“消费 BMI”
五、为什么 Modules 看起来“完美适合反射”?
你 slide 里的这几条逻辑是完全成立的:
No further context needed
BMI 本身就是完整类型上下文。
In theory, BMI always processed before .cpp
模块接口一定先编译。
所以理想模型是:
BMI ↓ Reflection Generator ↓ Generated .cpp ↓ Normal compilation用公式表示就是:
Module Interface → Compile BMI → Reflect Metadata Code \text{Module Interface} \xrightarrow{\text{Compile}} \text{BMI} \xrightarrow{\text{Reflect}} \text{Metadata Code}Module InterfaceCompileBMIReflectMetadata Code
从理论上,这几乎是完美的反射切入点。
六、为什么“理论成立,实践失败”?
关键在你 slide 的这一句:
In practice, no way to invoke mid compilation pass
这句话是整个问题的核心。
1⃣ 编译器不是“可插拔流水线”
现实中的编译器是:
- 单向
- 封闭
- 不暴露中间阶段
你不能做这样的事:
“编译器,停一下,把 BMI 给我,我生成点代码,再继续。”
原因:
- BMI 是内部格式
- 没有标准 API
- 不同编译器完全不同(
.ifc/.pcm/.gcm)
2⃣ BMI ≠ 反射输入格式
| 问题 | 现实 |
|---|---|
| BMI 是否标准 | 否 |
| 可否跨编译器 | 否 |
| 可否被用户代码访问 | 否 |
| 是否稳定 | 否 |
结论:
BMI 是给编译器用的,不是给程序或工具用的
七、Separate target = separation boundary(致命现实)
这是构建系统层面的硬边界。
1⃣ 构建系统看到的世界
Target A: Modules → BMI Target B: Sources → .obj中间:
- 没有“中途钩子”
- 没有“编译中回调”
- 没有“消费 BMI 再生成代码”的标准步骤
2⃣ 你图中绿色区域在表达什么?
你画的MyProject-ReflectionData:
BMI ↓ Reflection Generator(外部工具) ↓ Generated .cpp ↓ Compiler本质是:
反射生成被迫退化为“一个独立工程 / 独立 target”
八、这意味着什么?
1⃣ Modules 没有失败
Modules成功地:
- 集中类型信息
- 消除头文件解析
- 提升编译性能
2⃣ 但 Modules 不是反射方案
关键等式:
Modules = Reflection Enabler \text{Modules} = \text{Reflection Enabler}Modules=Reflection Enabler
但:
Modules ≠ Reflection Mechanism \text{Modules} \neq \text{Reflection Mechanism}Modules=Reflection Mechanism
九、真正缺失的那一块
缺的不是“类型信息”,而是:
语言级、标准化的“编译期语义访问能力”
也就是后来大家在做的:
- Reflection TS
std::meta- 编译期 AST 视图
十、一句话总结这一页(非常重要)
Modules 让反射在工程上第一次“看起来合理”,
但它依然无法让反射在语言层面“真正发生”。
一、.ifc是什么?
.ifc是C++ Modules 的二进制接口文件(Binary Module Interface, BMI),
在MSVC体系中叫IFC(Interface File Container)。
可以把
.ifc理解为:“编译器可直接读取的、结构化的 AST + 符号表 + 类型系统快照”
对比传统头文件:
传统.h | 模块.ifc |
|---|---|
| 文本 | 二进制 |
| 每次解析 | 一次生成,多次复用 |
| 宏、include | 语义级 import |
| 编译慢 | 编译快 |
二、.ifc文件的整体结构
你给出的结构是完全正确的,我们把它画成逻辑层次:
.ifc File ├─ Signature // 文件校验、版本、ABI 信息 ├─ Header // 全局索引入口 ├─ Partition[0] ├─ Partition[1] │ ├─ Types │ ├─ Decls │ ├─ Exprs │ └─ ... ├─ ... ├─ Partition[n] ├─ String Table // 所有字符串集中存储 └─ Table of Contents // TOC,定位各分区核心思想
.ifc不是一棵 AST,而是一组“索引化的语义对象池”
三、Header:全局“map”
你提到:
Header
“Map<str, Partition*>”
这是非常准确的理解。
Header 做什么?
Header 是:
Map<String,Partition*>逻辑上等价于:
"type.qualified" → Partition #3 "decl.function" → Partition #7 "expr" → Partition #9也就是说:
Header 告诉编译器:
- 某一类“实体(entity)”在哪个 Partition 里
- Partition 里再通过索引精确定位
四、Partition:语义实体的“分类仓库”
每个Partition存一类同构实体:
| Partition 名 | 内容 |
|---|---|
type.unqualified | 基本类型 |
type.qualified | const / volatile / ref 等 |
decl.struct | struct/class |
decl.function | 函数 |
expr | 表达式 |
stmt | 语句 |
| Partition 内部是“数组式存储” |
五、Qualified Type 的内部结构(你给的 Figure 9.16)
你给了这段关键描述:
E.g. partition
“type.qualified”
= QualifiedType[N];
unqualified: TypeIndex
qualifiers: Qualifiers
我们把它还原成结构体:
structQualifiedType{TypeIndex unqualified;// 去掉修饰后的类型Qualifiers qualifiers;// const / volatile / ref / ...};用数学形式表示
一个 QualifiedType 可以表示为:
T q = ⟨ T u , Q ⟩ T_q = \langle T_u,\ Q \rangleTq=⟨Tu,Q⟩
其中:
- T u T_uTu:unqualified type(例如
int) - Q QQ:限定符集合(例如
{const, &})
示例
constint&在.ifc里会变成:
QualifiedType { unqualified → TypeIndex(int) qualifiers → { const, lvalue_ref } }注意:
int本身只存一次
所有const int、int&、const int&都只是“引用 + 修饰”
六、AbstractIndex:.ifc的“万能指针”
你给出的这一段是理解 IFC 的关键:
AbstractIndex = 2x numbers
• 1 indexes which partition
• The other into that partition
抽象索引的定义
.ifc中的所有引用,几乎都通过一个AbstractIndex完成:
AbstractIndex = ( PartitionID , LocalIndex ) \text{AbstractIndex} = (\text{PartitionID},\ \text{LocalIndex})AbstractIndex=(PartitionID,LocalIndex)
位级布局(你给的图)
31 N N-1 0 +------------------+-------------------+ | index | tag / sort | +------------------+-------------------+你写的是:
tag: Sort (N]
index: Index[32-N]
也可以写成:
AbstractIndex = Sort ⏟ ∗ N b i t s ∥ Index ⏟ ∗ 32 − N b i t s \text{AbstractIndex} = \underbrace{\text{Sort}}*{N\ bits} \ \Vert \underbrace{\text{Index}}*{32-N\ bits}AbstractIndex=Sort∗Nbits∥Index∗32−Nbits
含义解释
- Sort(tag)
→ 表示“这是哪一类实体”- type
- decl
- expr
- Index
→ 在对应 Partition 中的数组下标
直观理解
(AbstractIndex) = “第 3 类 Partition 中的第 128 号对象”编译器看到一个 Index,不需要猜:
- 类型是什么?
- 存在哪?
一跳就到目标语义对象
七、String Table:去重 + 快速比较
.ifc中:
- 所有字符串(标识符、命名空间名)
- 都只存一次
- 用整数索引引用
例如:
"Entity" → StringID 42 "name" → StringID 57优点:
- 比较字符串 = 比较整数
- 省空间
- 快速反序列化
八、示例:EntityModule.ixx如何进入.ifc
源码
exportmodule EntityModule;exportstructUUID{uint64_tuuid;};exportstructEntity{inthealth;std::string name;UUID uuid;floatx,y,z;};在.ifc中会发生什么?
1⃣ Module 信息
- module 名:
EntityModule - 存在
decl.modulepartition
2⃣struct UUID
Decl.Struct ├─ name → "UUID" ├─ fields: │ └─ uuid : uint64_tuint64_t→ 基本类型 partitionUUID→ 一个 Decl.Struct Index
3⃣struct Entity
字段会被表示为:
health → TypeIndex(int) name → TypeIndex(std::string) uuid → TypeIndex(UUID) x,y,z → TypeIndex(float)注意
UUID并不是展开结构
而是通过 AbstractIndex 引用另一个 struct
九、Inspecting.ifc:Ifc SDK Visualizer
你提到:
Inspecting .ifc
• Ifc sdk visualizer
这是 MSVC 官方工具,作用是:
- 把
.ifc可视化成:- Partition
- Decl 树
- Type 图
- 验证模块导出是否正确
- 调试 module 编译问题
十、一句话总结(工程视角)
.ifc是一个:
- 去文本化
- 去宏化
- 去重复解析
的“C++ 语义数据库快照”
它的核心设计原则是:
Everything is an Index \text{Everything is an Index}Everything is an Index
一、整体在做什么(一句话版本)
Neat是一个:
基于 MSVC
.ifc的“编译期采集 + 运行期反射”系统它把模块接口里的语义信息转换成可链接、可查询、可修改的运行期元数据。
换句话说:
C++ Modules + IFC ⟹ Runtime Reflection \text{C++ Modules + IFC} \Longrightarrow \text{Runtime Reflection}C++ Modules + IFC⟹Runtime Reflection
二、Neat 的三个“Neatly”到底是什么意思
1⃣ implements simple, powerful reflection runtime library
这不是constexpr反射,也不是宏 hack,而是:
- 真·运行期
- 真·类型擦除
- 真·跨 DLL / EXE
你最终拿到的是:
constType*type=Neat::get_type<MyStruct>();也就是说:
Type ≈ C++ 类型的运行期描述对象 \text{Type} \approx \text{C++ 类型的运行期描述对象}Type≈C++类型的运行期描述对象
2⃣ generates data neatly listening to MSVC
这句话的关键在listening。
Neat 并不是“重新解析 C++”,而是:
- 监听MSVC 编译模块
- 读取
.ifc(Binary Module Interface) - 从中提取:
- struct
- field
- type
- layout
- offset
也就是:
IFC → Neat Generator Reflection Data (.cpp/.obj) \text{IFC} \xrightarrow{\text{Neat Generator}} \text{Reflection Data (.cpp/.obj)}IFCNeat GeneratorReflection Data (.cpp/.obj)
3⃣ integrates into your CMake project
核心是这行:
add_reflection_target(MyProject_ReflectionData MyProject)它的语义是:
“基于
MyProject生成一个伴生的反射数据目标”
三、构建期发生了什么(非常重要)
1⃣ 模块代码
exportstructMyStruct{intdamage;};这一步:
- MSVC 编译器
- 生成
.ifc - 其中包含:
MyStruct- 字段
damage - 类型
int - 字段偏移
2⃣ CMake:反射目标
add_library(MyProject "MyCode.ixx" "MyCode.cpp") add_reflection_target(MyProject_ReflectionData MyProject)逻辑等价于:
MyProject.ifc ↓ [Neat 解析] ↓ ReflectionData.cpp ↓ MyProject_ReflectionData.lib3⃣ 链接进最终程序
target_link_libraries(TheExe PUBLIC MyProject MyProject_ReflectionData )此时:
反射元数据 ≠ 魔法
它是普通的、已链接进程序的 C++ 对象
四、运行期:反射是如何工作的?
1⃣ 创建普通对象
MyStruct value{.damage=-5};内存状态:
value: +--------+ | damage | -5 +--------+2⃣ 获取类型对象
constType*type=Neat::get_type<MyStruct>();这里发生的是:
C++ 静态类型 T ⟶ TypeID ⟶ Type 对象 \text{C++ 静态类型 } T \longrightarrow \text{TypeID} \longrightarrow \text{Type 对象}C++静态类型T⟶TypeID⟶Type对象
即:
MyStruct → TypeID(0x1234) → Type*3⃣ AnyPtr:类型擦除的关键
AnyPtr value_ptr{&value,type->id};AnyPtr的本质是:
structAnyPtr{void*address;TypeID type;};数学上可以看作:
AnyPtr = ⟨ 地址 , 类型标识 ⟩ \text{AnyPtr} = \langle \text{地址},\ \text{类型标识} \rangleAnyPtr=⟨地址,类型标识⟩
它解决了一个核心问题:
“我有一块内存,但我不知道它的静态类型”
五、Type / Field 模型(反射的核心抽象)
Type
structType{TypeID id;std::vector<Field>fields;};它表示:
Type = f 0 , f 1 , … , f n \text{Type} = { f_0, f_1, \dots, f_n }Type=f0,f1,…,fn
Field
structField{std::string name;TypeID type;size_t offset;};字段在对象中的地址计算公式是:
$$
\text{field_address}
\text{object_address} + \text{offset}
$$
六、关键操作:通过反射修改字段
1⃣ 拿到字段
constField&field=type->fields[0];即:
fields[0]→damage
2⃣ set_value 的语义
field.set_value(value_ptr,75);展开逻辑:
- 检查
value_ptr.type == MyStruct - 计算字段地址:
a d d r = v a l u e _ p t r . a d d r e s s + f i e l d . o f f s e t addr = value\_ptr.address + field.offsetaddr=value_ptr.address+field.offset - 将
75写入该地址
等价于:
*(int*)((char*)&value+offset)=75;但带有类型系统约束。
3⃣ 验证结果
assert(value.damage==75);这说明:
反射并不是“复制”数据,而是直接操作对象内存
七、为什么这是“安全的”反射?
与传统 C 风格反射相比,Neat 有几个关键安全点:
1⃣ 类型 ID 校验
AnyPtr.type = ? Field.owner_type \text{AnyPtr.type} \overset{?}{=} \text{Field.owner\_type}AnyPtr.type=?Field.owner_type
避免对错误对象写字段。
2⃣ 字段偏移来自编译器
- 不靠
offsetofhack - 不靠 ABI 猜测
- 直接来自
.ifc
3⃣ 无宏、无侵入
exportstructMyStruct{...};不需要:
REFLECT()REGISTER_TYPE()X-Macro
八、这套方案在 C++ 生态中的定位
可以这样理解:
| 方案 | 特点 |
|---|---|
| 宏反射 | 侵入性强 |
constexpr反射 | 编译期,不能跨 DLL |
| Clang tooling | 重 |
| Neat + IFC | 轻、准、快、模块友好 |
九、总结一句“架构级”的话
Neat 的本质是:
把C++ Modules 编译器内部的语义真相
变成用户可控的运行期数据结构
公式化表达:
IFC (compiler truth) → Neat Runtime Type System \text{IFC (compiler truth)} \xrightarrow{\text{Neat}} \text{Runtime Type System}IFC (compiler truth)NeatRuntime Type System
一、Trivial IFC limitations(“看起来很烦,但本质不深”的限制)
这些限制并不是 IFC 设计失败,而是“它只解决它该解决的问题”。
1⃣ Only modules(只支持 Modules)
现象
- 只有
export module ...产生.ifc - 传统
.h / .cpp没有 IFC
原因(非常关键)
IFC 是模块 ABI 的一部分,而不是通用 AST dump
Modules 的核心承诺是:
Interface ; ⟹ ; Stable, structured, importable \text{Interface} ;\Longrightarrow; \text{Stable, structured, importable}Interface;⟹;Stable, structured, importable
而头文件:
- 没有语义边界
- 有宏
- 有条件编译
- 有 include 顺序依赖
无法形成可靠的“接口快照”
对反射库的影响
- 你的反射系统天然是 module-first
- 这其实是优点,而不是缺点
- 等价于强制用户写“干净接口”
2⃣ No user attributes(没有用户自定义属性)
现象
[[my_reflect(skip)]]structA{};→ 在 IFC 里不存在
原因
IFC 当前保存的是:
- 语言核心语义
- ABI 相关信息
而不是: - tooling metadata
- 框架扩展点
也就是说:
IFC ≠ Clang AST with plugins \text{IFC} \neq \text{Clang AST with plugins}IFC=Clang AST with plugins
对反射库的影响
- 你不能依赖属性做标记
- 只能用:
- 命名约定
- module / partition 边界
- 类型结构本身
这迫使库设计变成:
“结构驱动反射”而不是“注解驱动反射”
3⃣ BMI filename query(BMI 文件名不可查询)
现象
- 你不知道:
import X;- 对应的
.ifc文件路径
原因
这是刻意的设计选择:
BMI 是编译器内部缓存,不是用户接口
文件名、位置、缓存策略都是:
- 编译器私有
- 可随版本变化
对反射工具的影响
你的工具必须:
- 通过编译器调用链
- 而不是:
- 猜路径
- 扫磁盘
二、Non-trivial limitations(真正“硬”的限制)
这些限制才是会影响反射能力边界的。
1⃣ Templates not instantiated(模板未实例化)
现象
template<typenameT>structBox{T value;};在 IFC 中:
- 只有模板定义
- 没有:
Box<int>Box<float>
原因(语言层面)
模板实例化规则是:
Template + Use ⟹ Instantiation \text{Template} + \text{Use} \Longrightarrow \text{Instantiation}Template+Use⟹Instantiation
而 IFC 只描述:
- 接口
- 潜在语义
它不承担: - “猜测你将来会用什么 T”
对运行期反射的影响
这意味着:
IFC 不能直接告诉你“所有类型”
你的库只能反射:
- 非模板类型
- 或已具体化的类型
2⃣ Compiler specific(编译器特定)
现象
.ifc:MSVC.pcm:Clang.gcm:GCC
结构、语义、索引规则都不同。
原因(现实)
C++ 没有标准化的 BMI 格式
各家编译器:
- 用 modules
- 但不共享内部表示
对库作者的含义
你的库实际上是:
Reflection Library + Compiler Backend \text{Reflection Library} + \text{Compiler Backend}Reflection Library+Compiler Backend
而不是一个单一实现。
3⃣ IPR(Intermediate Program Representation)
你给了链接:
https://github.com/GabrielDosReis/ipr
这是一个重要信号。
IPR 的意义
- 抽象:
- IFC / PCM / GCM
- 提供:
- 统一语义模型
- 面向:
- 工具
- 反射
- 分析器
可以理解为:
Compiler Internal IR ; ⟹ ; Tool-facing IR \text{Compiler Internal IR} ;\Longrightarrow; \text{Tool-facing IR}Compiler Internal IR;⟹;Tool-facing IR
但它:
- 仍在探索中
- 尚未成为标准
三、Just Compile Time Reflection Things(但跑到了 runtime)
这页其实是哲学层面的总结。
1⃣ “But then in runtime reflection land”
你做的事情是:
在编译期“看到”类型
在运行期“使用”类型
形式化表达:
Compile-time Type Knowledge ; → materialize Runtime Type Objects \text{Compile-time Type Knowledge} ;\xrightarrow{\text{materialize}} \text{Runtime Type Objects}Compile-time Type Knowledge;materializeRuntime Type Objects
2⃣ During registration you have the type
这是非常关键的一句。
含义
在生成反射数据时:
- 你已经知道:
- 类型
- 字段
- 布局
- offset
因此:
- 不需要 RTTI hack
- 不需要
typeid - 不需要
dynamic_cast
3⃣ Design of library meant to be rebuild using library pieces
这句话体现的是成熟库设计思维。
含义
你的反射库:
- 不是一个 monolith
- 而是:
- Type system
- Registry
- AnyPtr
- Field ops
- Backend (IFC)
可以表示为:
Reflection System = = = = = = = = = = = = = = = = = = = = = = = = ∑ i = 1 n Composable Components \text{Reflection System} ======================== \sum_{i=1}^{n} \text{Composable Components}Reflection System========================i=1∑nComposable Components
四、把三页合在一起的一句话总结
IFC 不是“完美反射源”,
但它是“编译器愿意稳定提供的最强接口事实来源”。
而你的设计选择是:
- 接受:
- 模块限定
- 模板边界
- 编译器差异
- 换取:
- 精确类型布局
- 无宏
- 真运行期反射
- 工程级可用性
五、一句“老 C++ 人”才会说的话
This is just compile-time reflection things…
except you actually ship it to runtime.
一、为什么一定要有 Type Registry?
在运行期反射中,有一个不可回避的核心问题:
“我怎么从一个名字 / ID,找到一个类型的完整描述?”
这就是Type Registry(类型注册表)的职责。
你给出的设计:
externstd::unordered_map<std::string,Type>type_registry;可以理解为:
TypeRegistry : TypeName ⟶ Type \text{TypeRegistry} : \text{TypeName} \longrightarrow \text{Type}TypeRegistry:TypeName⟶Type
它是整个反射系统的“全局真相表(single source of truth)”。
二、Registry 存的到底是什么?
1⃣ 存的是“类型对象”,不是 C++ 类型本身
Type不是T,而是T的运行期语义描述。
2⃣ 为什么用std::string作为 key?
std::unordered_map<std::string,Type>含义是:
- 类型名是人类可理解的入口
- 可用于:
- 调试
- 序列化
- 脚本绑定
- 编辑器
数学抽象:
name ∈ Σ ∗ ⇒ Type \text{name} \in \Sigma^* \Rightarrow \text{Type}name∈Σ∗⇒Type
三、register_type() 在“注册”什么?
你给出的是:
template<typenameT>voidregister_type(Field[],Method[]);1⃣ register_type 的角色
把“编译期已知的类型 T”
变成“运行期可查询的 Type 实例”
形式化描述:
T compile → register_type Type runtime T_{\text{compile}} \xrightarrow{\text{register\_type}} \text{Type}_{\text{runtime}}Tcompileregister_typeTyperuntime
2⃣ register_type 内部逻辑(概念版)
template<typenameT>voidregister_type(Field fields[],Method methods[]){Type type;type.name=/* "T" */;type.fields=/* pointers to fields */;type.methods=/* pointers to methods */;type_registry[type.name]=std::move(type);}3⃣ 为什么 fields / methods 作为参数传入?
因为:
- 字段、方法信息:
- 来自 IFC
- 或生成代码
- 不是由 register_type 解析出来的
register_type是装配(assembly)阶段,不是采集阶段。
四、Type / Field / Method 的关系模型
你给出的结构是:
structField;structMethod;structType{std::string name;std::vector<Field*>fields;std::vector<Method*>methods;};我们把它提升到抽象层。
1⃣ Type 是“中心节点”
数学上:
Type = ⟨ name , F , M ⟩ \text{Type} = \langle \text{name},\ F, M \rangleType=⟨name,F,M⟩
其中:
- F = f 1 , f 2 , … , f n F = { f_1, f_2, \dots, f_n }F=f1,f2,…,fn(字段集合)
- M = m 1 , m 2 , … , m k M = { m_1, m_2, \dots, m_k }M=m1,m2,…,mk(方法集合)
2⃣ Field / Method 是“可共享的描述对象”
你用的是:
std::vector<Field*>std::vector<Method*>这隐含了一个重要设计点:
Field / Method 不是 Type 的子对象,而是独立实体
3⃣ 为什么不用vector<Field>?
原因通常有三种:
- 共享
- 继承
- 模板实例
- 稳定地址
- Registry 重建
- DLL 边界
- 避免拷贝
从语义上:
Type ; → has-a ; Field \text{Type} ;\xrightarrow{\text{has-a}}; \text{Field}Type;has-a;Field
而不是:
Type ; → owns ; Field \text{Type} ;\xrightarrow{\text{owns}}; \text{Field}Type;owns;Field
五、Field / Method 通常还会有什么?
虽然你没贴出来,但在这种系统里,几乎必然包含:
Field
structField{std::string name;TypeID type;size_t offset;};字段地址计算公式:
$$
\text{addr}_{field}
\text{addr}_{object} + \text{offset}
$$
Method
structMethod{std::string name;TypeID return_type;std::vector<TypeID>params;void(*invoke)(AnyPtr,Args...);};抽象表示:
m : ( T , P 1 , … , P n ) → R m : (T, P_1, \dots, P_n) \rightarrow Rm:(T,P1,…,Pn)→R
六、注册表在运行期的典型使用方式
1⃣ 通过名字查类型
Type&t=type_registry["MyStruct"];2⃣ 遍历字段(编辑器 / 序列化)
for(Field*f:t.fields){// UI / JSON / Binary}3⃣ 方法调用(脚本 / RPC)
Method*m=t.methods[0];m->invoke(obj,args...);七、设计上的几个“隐藏但关键”的点
1⃣ Registry 是“全局状态”
这意味着你要考虑:
- 初始化顺序
- 重复注册
- DLL 边界
2⃣ Type 对象是否可变?
如果允许修改:
Type ∗ t 0 ≠ Type ∗ t 1 \text{Type}*{t_0} \neq \text{Type}*{t_1}Type∗t0=Type∗t1
你就必须保证:
- 所有指针仍然有效
- 没有数据竞争
3⃣ name ≠ identity(重要)
理想设计通常是:
- name:人类接口
- TypeID:机器接口
即:
name → TypeID → Type \text{name} \rightarrow \text{TypeID} \rightarrow \text{Type}name→TypeID→Type
八、一句话总结(架构层)
Type Registry 是运行期反射系统的“内存中的编译器符号表”。
而你这套设计的本质是:
Compiler IFC Truth → Codegen Type / Field / Method → Registry Runtime Reflection \text{Compiler IFC Truth} \xrightarrow{\text{Codegen}} \text{Type / Field / Method} \xrightarrow{\text{Registry}} \text{Runtime Reflection}Compiler IFC TruthCodegenType / Field / MethodRegistryRuntime Reflection
一、普通成员访问在“语义上”做了什么?
先看你给的最普通代码:
Entity*some_entity=GetSomeEntity();std::cout<<(*some_entity).name;编译器眼里的真实含义
这行代码在语义上等价于:
- 取得对象基地址:
some_entity - 取得成员
name在Entity内的偏移 - 计算成员地址并访问
用数学形式表示:
addr ( name ) = addr ( entity ) + offset name \text{addr}(\text{name}) = \text{addr}(\text{entity}) + \text{offset}_{\text{name}}addr(name)=addr(entity)+offsetname
这里:
- offset name \text{offset}_{\text{name}}offsetname是编译期常量
- 对用户是不可见的
二、“Reverse of member access”:反转访问方向
关键一句是:
Reverse of member access
意思是:
不是“对象 → 成员”,而是“成员 → 对象”
把成员变成变量
autosome_member=GetSomeMember();Entity entity;std::cout<<entity.*some_member;这里发生了什么?
some_member不再是名字- 而是一个值(value)
- 它描述了:
- 如何从一个
Entity得到某个成员
- 如何从一个
这正是 C++ 的 pointer-to-member
std::string Entity::*ptr=&Entity::name;entity.*ptr;语义上:
value = entity ∘ member_descriptor \text{value} = \text{entity} \circ \text{member\_descriptor}value=entity∘member_descriptor
三、Relative Pointer(相对指针)的核心思想
Relative pointer
这句话非常关键。
什么是 relative?
- 不是:
std::string*
- 而是:
- “相对于对象起始地址的成员位置”
用结构体说明
structEntity{inthealth;std::string name;floatx,y,z;};内存布局(示意):
+-----------------+ | health (int) | offset = 0 +-----------------+ | name (string) | offset = 4 +-----------------+ | x (float) | offset = 36 | y (float) | offset = 40 | z (float) | offset = 44 +-----------------+Relative pointer 的数学定义
Field = ⟨ offset , type ⟩ \text{Field} = \langle \text{offset},\ \text{type} \rangleField=⟨offset,type⟩
访问公式:
$$
\text{field_addr}
\text{object_addr} + \text{offset}
$$
四、Field 抽象:把“成员”变成数据
反射系统里的Field,本质上就是:
“可运行期使用的 pointer-to-member”
一个典型的 Field 定义
structField{std::string name;TypeID type;size_t offset;};这里的offset:
- 就是 relative pointer
- 和
&Entity::name在语义上等价 - 但:
- 可序列化
- 可存储
- 可跨模块
Field 的访问逻辑
void*Field::get_address(void*object)const{returnstatic_cast<char*>(object)+offset;}数学形式:
f ( o ) = o + offset f f(o) = o + \text{offset}_ff(o)=o+offsetf
五、为什么不用 C++ 原生 pointer-to-member?
这是一个反射系统必须面对的问题。
原生 pointer-to-member 的问题
- 实现相关
- 大小不固定
- ABI 不稳定
- 不可序列化
- 跨 DLL 不可靠
- 无法动态生成
Relative offset 的优势
| 属性 | pointer-to-member | offset |
|---|---|---|
| ABI 稳定 | X | √ |
| 可存储 | X | √ |
| 可生成 | X | √ |
| 来自 IFC | X | √ |
六、Field abstraction = “反射级成员访问”
你可以把 Field 看成一个函数:
f : Object → Subobject f : \text{Object} \rightarrow \text{Subobject}f:Object→Subobject
而set_value/get_value是:
f ( o ) : = v f(o) := vf(o):=v
这正是你前面反射例子中:
field.set_value(value_ptr,75);的理论基础。
七、把这页 slide 总结成一句话
Field abstraction 的本质是:
把“编译期绑定的成员访问”
变成“运行期可传递、可存储、可计算的相对地址函数”
公式版总结:
member access = object + offset \text{member access} = \text{object} + \text{offset}member access=object+offset
八、和你整个 IFC + 反射体系的关系
- IFC 提供:
- 精确 offset
- 你生成:
- Field{ offset, type }
- Registry 保存:
- Field*
- Runtime 使用:
- AnyPtr + Field
形成闭环:
IFC → Field → Runtime Access \text{IFC} \rightarrow \text{Field} \rightarrow \text{Runtime Access}IFC→Field→Runtime Access
- AnyPtr + Field
一、这页真正想解决的是什么问题?
标题写得很直白:
Type erasing a member variable
成一句“系统级”的话就是:
如何把一个“编译期绑定的成员变量”,
变成一个“运行期可统一操作的对象”?
数学化一点:
Member of T ; ⟶ ; Field (runtime value) \text{Member of } T ;\longrightarrow; \text{Field (runtime value)}Member ofT;⟶;Field (runtime value)
二、第一反应:基类 + 虚函数(非常自然)
你写了:
classField{public:virtual~Field()=default;virtualstd::string_viewname()=0;virtualconstType*type()=0;virtualAnyRefvalue(void*object)=0;};这一步在抽象什么?
这个Field接口,实际上定义了一个运行期“成员访问协议”:
| 方法 | 含义 |
|---|---|
name() | 成员的名字 |
type() | 成员的类型 |
value(object) | 从某个对象中取出该成员 |
从数学上看,value是一个函数: | |
| $$ | |
| \text{value}_f : \text{Object} \rightarrow \text{AnyRef} | |
| $$ |
void* object的意义
这是类型擦除的关键点:
Field不能知道对象的静态类型- 只能接受:
void*
因此:
Object ≡ void* \text{Object} \equiv \text{void*}Object≡void*
三、实现 Field:FieldImpl 的核心思想
你给出的实现是:
classFieldImpl:publicField{public:usingPtrToMember=intEntity::*;PtrToMember ptr_to_member;AnyRefvalue(void*object)override{Entity*entity_object=static_cast<Entity*>(object);int*field_ptr=&(entity_object->*ptr_to_member);returnAnyRef{field_ptr,int_type};}};这段代码非常“教科书级”,我们逐层拆。
四、PtrToMember:C++ 原生的“成员描述符”
usingPtrToMember=intEntity::*;这不是普通指针,而是pointer-to-member。
语义不是“地址”,而是“规则”
它表示的是:
“如何从一个
Entity对象中,定位到一个int成员”
数学上可以理解为:
ptr_to_member : Entity → int \text{ptr\_to\_member} : \text{Entity} \rightarrow \text{int}ptr_to_member:Entity→int
成员访问的展开
entity_object->*ptr_to_member在语义上等价于:
$$
\text{addr}(\text{member})
\text{addr}(\text{entity}) + \text{offset}
$$
只是这个offset被封装在ptr_to_member里。
五、value(void*):反射访问的完整流程
我们逐行解释:
Entity*entity_object=static_cast<Entity*>(object);把类型擦除的 object 恢复成具体类型
这是一个前提假设:
调用者保证:
object确实指向一个Entity
int*field_ptr=&(entity_object->*ptr_to_member);这一步做的是:
- 使用成员指针定位字段
- 取字段地址
数学表达:
field_ptr = entity_ptr ∘ ptr_to_member \text{field\_ptr} = \text{entity\_ptr} \circ \text{ptr\_to\_member}field_ptr=entity_ptr∘ptr_to_member
returnAnyRef{field_ptr,int_type};这一步是运行期反射的“交付时刻”:
AnyRef = ⟨ address , Type ⟩ \text{AnyRef} = \langle \text{address},\ \text{Type} \rangleAnyRef=⟨address,Type⟩
address:字段在内存中的位置Type:字段的运行期类型描述
六、这套设计“正确”的地方
1⃣ 真正完成了类型擦除
Field层:
- 不知道
Entity - 不知道
int - 只认
void*和Type*
2⃣ 行为是“反射级别”的
Field::value本质上就是:
f ( o ) = AnyRef f(o) = \text{AnyRef}f(o)=AnyRef
你可以:
- 读
- 写
- 序列化
- 绑定脚本
3⃣ 实现直观、易验证
- 没有指针算术
- 没有 UB(前提成立)
- 适合教学和原型
七、但:这是“第一反应”,问题也很明显
这页 slide 的标题其实已经暗示了:
1st instinct
问题 1⃣:FieldImpl 强绑定到 Entity
usingPtrToMember=intEntity::*;意味着:
- 一个 FieldImpl
- 只能服务一个具体
Entity - 一个字段类型 = 一个类
FieldImpl 数量爆炸
问题 2⃣:pointer-to-member 的 ABI 问题
- 大小不固定
- 编译器实现相关
- 多继承 / 虚继承更复杂
这在跨 DLL / 跨编译器的反射系统里是个隐患。
问题 3⃣:虚函数 + 小对象,开销不低
每次字段访问:
- 虚函数分发
- 成员指针解引用
- AnyRef 构造
对 editor / tools OK
对 hot loop
八、为什么你前面强调 “relative pointer”
结合你前一页 slide,其实你自己已经在“推翻”这个方案了 😄
更稳定的模型是:
structField{size_t offset;constType*type;};访问变成:
field_addr = object_addr + offset \text{field\_addr} = \text{object\_addr} + \text{offset}field_addr=object_addr+offset
- 无 ABI 风险
- 可序列化
- 来自 IFC
- 不依赖
Entity::*
九、这页在整个反射设计中的地位
你这页的意义不是“最终方案”,而是:
说明“最自然的 C++ 面向对象设计”,
为什么在反射系统中会遇到边界。
它起到的是:
- 思维过渡
- 设计对比
- 教育读者“为什么需要更底层的抽象”
十、一句话总结(非常重要)
这页展示的是:
如何用 C++ 语言自身的机制(virtual + pointer-to-member)
第一次触碰“运行期字段访问”这个问题而后续的设计,正是对它的“工程化升级”。
公式版总结:
Field : void* → AnyRef \text{Field} : \text{void*} \rightarrow \text{AnyRef}Field:void*→AnyRef
一、第一版:模板化 FieldImpl(泛化成员类型)
代码
template<typenameTObject,typenameTField>classFieldImpl:publicField{public:usingPtrToMember=TField TObject::*;// DataPtrToMember ptr_to_member;// FunctionsAnyRefvalue(void*object)override{TObject*typed_object=static_cast<TObject*>(object);TField*field_ptr=&(typed_object->*ptr_to_member);returnAnyRef{field_ptr,field_type};}};这一步解决了什么?
相比最早写死Entity/int的版本,这一版完成了:
字段“类型 + 所属对象类型”的完全泛化
数学上:
FieldImpl ⟨ T object , T field ⟩ \text{FieldImpl}\langle T_{\text{object}}, T_{\text{field}} \rangleFieldImpl⟨Tobject,Tfield⟩
表示的是一个函数:
f ( o ) = address of ( o . T field ) f(o) = \text{address of } (o.T_{\text{field}})f(o)=address of(o.Tfield)
类型擦除在哪里?
Field接口:AnyRefvalue(void*object)- 类型恢复发生在这里:
static_cast<TObject*>(object)
也就是说:
void* → static_cast T object ∗ \text{void*} \xrightarrow{\text{static\_cast}} T_{\text{object}}^*void*static_castTobject∗
前提仍然是:调用者必须传对对象类型。
二、Finished Field Implementation:补齐“反射语义”
新增内容
std::string field_name;Type*field_type;以及:
std::string_viewname()override{returnfield_name;}constType*type()override{returnfield_type;}这一版的意义
这一步非常重要,因为它标志着:
FieldImpl 不再只是“访问器”,而是“反射对象”
现在一个Field完整描述了:
Field = ⟨ name , type , accessor ⟩ \text{Field} = \langle \text{name},\ \text{type},\ \text{accessor} \rangleField=⟨name,type,accessor⟩
这正是反射系统中“字段”的最小语义闭包。
三、Simplifying Field:把“成员指针”变成编译期常量
代码
template<typenameTObject,typenameTField,TField TObject::*PtrToMember>classFieldImpl:publicField{public:// Datastd::string field_name;Type*field_type;// Functionsstd::string_viewname()override{returnfield_name;}constType*type()override{returnfield_type;}AnyRefvalue(void*object)override{TObject*typed_object=static_cast<TObject*>(object);TField*field_ptr=&(typed_object->*PtrToMember);returnAnyRef{field_ptr,field_type};}};这一步“简化”的本质是什么?
你做了一个非常关键的转变:
把
ptr_to_member从运行期数据,提升为模板非类型参数
也就是说:
- 之前:
PtrToMember ptr_to_member;// 运行期成员 - 现在:
TField TObject::*PtrToMember// 编译期常量
带来的好处
1⃣FieldImpl 对象更小
之前:{ vptr, ptr_to_member, field_name, field_type } 现在:{ vptr, field_name, field_type }2⃣更容易内联 / 优化
编译器知道PtrToMember是常量:
( o → ∗ P t r T o M e m b e r ) ; 可内联 (o \rightarrow^* PtrToMember) ;\text{可内联}(o→∗PtrToMember);可内联
3⃣语义更“静态”
这个 FieldImpl只能代表一个确定的字段
四、走到这里,你已经“几乎”不需要继承了
注意你现在的 FieldImpl:
- 唯一“动态”的行为是:
value(void*)
- 但这个行为:
- 完全由模板参数决定
- 没有真正的多态逻辑
这自然引出了下一页。
五、最终简化:Field 变成“纯数据结构”
你给出的目标形态
structField{// Datastd::string name;Type*type;// FunctionsAnyRefvalue(void*object){// ???// Where type erasure}};这一步卡住的地方,正是整条演进链的核心问题。
六、为什么这里会卡住?
问题的本质
你现在想要的是:
一个非模板、非虚函数的 Field,
却仍然能“访问任意类型对象的任意字段”
也就是说,你想要:
Field : void* → AnyRef \text{Field} : \text{void*} \rightarrow \text{AnyRef}Field:void*→AnyRef
但此时:
TObject消失了TField消失了PtrToMember也消失了
所有类型信息都被抹掉了
七、缺失的那一块是什么?
答案是:访问策略(access strategy)
在前面的所有版本里,真正完成访问的是这一步:
&(typed_object->*PtrToMember)一旦你删掉模板参数,就必须用别的东西来表达:
field_addr = object_addr + ??? \text{field\_addr} = \text{object\_addr} + \text{???}field_addr=object_addr+???
八、两条必然的出路(这正是你后面会讲的)
出路 1:relative offset(你前面已经铺垫)
structField{std::string name;Type*type;size_t offset;};AnyRefField::value(void*object){returnAnyRef{static_cast<char*>(object)+offset,type};}数学表达:
f ( o ) = o + offset f(o) = o + \text{offset}f(o)=o+offset
这是IFC 驱动反射的最终形态。
出路 2:函数指针 / lambda(不如 offset)
usingAccessFn=AnyRef(*)(void*);AccessFn access;但:
- 有间接调用
- 不能序列化
- ABI 更复杂
九、这几页真正想传达的“设计结论”
你这一连串 slide 在讲的不是代码,而是一个结论:
C++ 语言级抽象(继承 / 模板 / pointer-to-member)
最终会逼你回到“偏移 + 类型”这一底层模型。
公式版总结:
Field = ⟨ name , type , offset ⟩ \text{Field} = \langle \text{name},\ \text{type},\ \text{offset} \rangleField=⟨name,type,offset⟩
十、一句话总结(非常重要)
你不是“不知道怎么实现
value()”,
而是已经把所有“类型信息”剥干净了,
只剩下“地址计算”这一条路。
而这,正是IFC 能完美补上的那一块。
如果你愿意,下一步我可以直接帮你写出:
一、为什么要 De-virtualize(去虚函数)?
在前面的设计中,你经历了:
Field是基类FieldImpl<TObject, TField>继承virtual AnyRef value(void*)
这在语义上是对的,但在工程上有几个问题:
- 每次访问字段都有虚函数分发
- 每个字段一个对象 + vptr
- 很难内联
- 对 cache / 热路径不友好
而你真正需要的其实只有一件事:
“给我一个
void*,我能算出字段 / 调用方法”
也就是说,你要的是:
f : void* → AnyRef f : \text{void*} \rightarrow \text{AnyRef}f:void*→AnyRef
而不是运行期多态。
二、De-virtualized Field:把“行为”变成数据
1⃣ 新的 Field 结构
structField{// Datastd::string name;Type*type;// FunctionsusingValueFunc=AnyRef(*)(void*object);ValueFunc value;};核心变化
| 旧设计 | 新设计 |
|---|---|
virtual AnyRef value(void*) | AnyRef (*value)(void*) |
| vtable | 函数指针 |
| 继承层级 | 纯数据 |
Field 变成了:数据 + 行为指针
数学抽象:
Field = ⟨ name , type , f ⟩ \text{Field} = \langle \text{name},\ \text{type},\ f \rangleField=⟨name,type,f⟩
其中:
f ( o ) = AnyRef f(o) = \text{AnyRef}f(o)=AnyRef
三、Field 的 value_func:编译期绑定,运行期调用
模板实现
template<typenameTObject,typenameTField,TField TObject::*PtrToMember>AnyRefvalue_func(void*object){TObject*typed_object=static_cast<TObject*>(object);TField*field_ptr=&(typed_object->*PtrToMember);returnAnyRef{field_ptr,field_type};}这段代码在“反射层面”做了什么?
1⃣ 类型恢复(唯一的危险点)
TObject*typed_object=static_cast<TObject*>(object);这是约定型安全:
- registry 保证
Field只用于对应TObject
2⃣ 成员访问的本质
&(typed_object->*PtrToMember)等价于地址计算:
$$
\text{field_addr}
\text{object_addr} + \text{offset}_{\text{field}}
$$
只是 offset 被封装在PtrToMember里。
3⃣ 返回 AnyRef(类型擦除完成)
AnyRef{field_ptr,field_type}数学形式:
AnyRef = ⟨ address , Type ⟩ \text{AnyRef} = \langle \text{address},\ \text{Type} \rangleAnyRef=⟨address,Type⟩
关键设计点总结(Field)
- 模板决定一切
- 运行期只做一次函数指针调用
- 没有虚函数
- 没有 RTTI
- 没有 dynamic_cast
四、De-virtualized Method:把“成员函数调用”也变成函数指针
1⃣ Method 结构
structMethod{// Datastd::string method_name;Type*method_return_type;std::vector<Type*>method_parameter_types;// FunctionsusingInvokeFunc=AnyRef(*)(void*,std::span<void*>);InvokeFunc invoke;};抽象含义
一个 Method 表示:
m : ( Object , Args ) → AnyRef m : (\text{Object}, \text{Args}) \rightarrow \text{AnyRef}m:(Object,Args)→AnyRef
- 参数是
void*+void*[] - 返回值也是 type-erased
五、invoke_func:反射调用的核心算法
模板定义
template<autoPtrToMemberFunction,typenameTObject,typenameTReturn,typename...TParams>AnyRefinvoke_func(void*object,std::span<void*>arguments)1⃣ 编译期安全校验
static_assert(std::is_same_v<decltype(PtrToMemberFunction),TReturn(TObject::*)(TParams...)>);这是非常重要的一行。
它保证:
PtrToMemberFunction : ( T O b j e c t : : ∗ ) ( T P a r a m s . . . ) → T R e t u r n \text{PtrToMemberFunction} : (TObject::*)(TParams...) \rightarrow TReturnPtrToMemberFunction:(TObject::∗)(TParams...)→TReturn
如果模板参数不一致,直接编译失败
2⃣ 类型恢复
TObject*typed_object=static_cast<TObject*>(object);与 Field 一样,这是约定型安全。
3⃣ 参数展开(最精彩的部分)
autoinvoke_internal=[&]<size_t...Indices>(std::index_sequence<Indices...>){return(typed_object->*PtrToMemberFunction)(*static_cast<TParams*>(arguments[Indices])...);};这里发生了什么?
arguments是:void*arguments[N]TParams...是:(P0,P1,...,PN)
展开后等价于:
typed_object->method(*static_cast<P0*>(arguments[0]),*static_cast<P1*>(arguments[1]),...);数学表示
$$
m(o, a_0, \dots, a_n)
(o \rightarrow^* f)(*a_0, \dots, *a_n)
$$
4⃣ 返回值处理
returninvoke_internal(std::index_sequence_for<TParams...>{});- 如果
TReturn是值类型 - 你通常会:
- 拷贝到 storage
- 再返回
AnyRef
(slide 中为简化省略了这一层)
六、这一设计相比虚函数方案的优势
性能层面
| 项目 | 虚函数 | 函数指针 |
|---|---|---|
| 分发成本 | 高 | 低 |
| 可内联 | (部分) | |
| 对 cache | 一般 | 更友好 |
架构层面
- Field / Method 是POD-like
- 易于:
- 存 registry
- 跨模块
- 序列化
- 行为由模板生成,逻辑集中
七、但:这仍然不是“最终形态”
你应该已经注意到了:
- Field 仍依赖
PtrToMember - Method 仍依赖
PtrToMemberFunction
这意味着: - ABI 仍是 C++ 私有的
- 跨编译器仍不可行
- IFC 里其实给了你更好的东西……
八、和你整个 IFC 反射路线的关系
你现在正处在这条路径的倒数第二步:
virtual Field ↓ templated FieldImpl ↓ function pointer Field ↓ (offset + type) ← IFC 最终解九、一句话总结(非常关键)
De-virtualization 的本质不是“少用 virtual”,
而是:把“多态”前移到编译期
把“运行期行为”压缩成一次函数指针跳转
公式化总结:
Runtime Polymorphism ⟶ Compile-time Specialization \text{Runtime Polymorphism} \longrightarrow \text{Compile-time Specialization}Runtime Polymorphism⟶Compile-time Specialization
一、给Type增加构造 / 析构能力
1⃣ 为什么反射系统需要 Constructor / Destructor?
如果你的反射系统只做:
- 枚举字段
- 调用方法
那它只是introspection(自省)。
但一旦你想做下面这些事:
- 反射创建对象(工厂)
- 脚本 / 编辑器创建 C++ 对象
- 序列化 / 反序列化
- ECS / 资源系统
你就必须回答一个问题:
“我怎么在不知道具体类型
T的情况下构造 / 析构对象?”
2⃣ 扩展后的Type结构
structType{// Datastd::string name;std::vector<Field*>fields;std::vector<Method*>methods;// FunctionsusingConstructorFunc=void(*)(void*);ConstructorFunc constructor;usingDestructorFunc=void(*)(void*);DestructorFunc destructor;};抽象意义
Type现在不仅描述结构,还描述生命周期:
Type = ⟨ name , fields , methods , construct , destruct ⟩ \text{Type} = \langle \text{name},\ \text{fields},\ \text{methods}, \text{construct},\ \text{destruct} \rangleType=⟨name,fields,methods,construct,destruct⟩
3⃣ 类型擦除的构造函数
template<typenameT>voiderased_constructor(void*object_memory){new(object_memory)T();}关键点解析
object_memory是一块已分配但未构造的内存- 使用placement new
- 构造发生在调用点,不依赖 RTTI
数学表示:
construct T ( p ) : = new ( p ) T ( ) \text{construct}_T(p) := \text{new}(p)\ T()constructT(p):=new(p)T()
4⃣ 类型擦除的析构函数
template<typenameT>voiderased_destructor(void*object){static_cast<T*>(object)->~T();}重要细节
- 只调用析构
- 不释放内存
- 内存管理策略由上层决定
destruct T ( p ) : = p → ∼ T ( ) \text{destruct}_T(p) := p \rightarrow \sim T()destructT(p):=p→∼T()
5⃣ 为什么这套设计很“C++”?
- 无虚函数
- 无 RTTI
- 生命周期完全由反射系统接管
- 非常适合:
- GameDev
- Embedded
- 自定义 allocator
二、TypeId:不用 RTTI 的“类型识别”
1⃣ 为什么不能用typeid(T)?
虽然 C++ 提供:
typeid(T)但在很多工程里:
- RTTI 被关闭(
-fno-rtti) typeid跨 DLL / SO 行为不稳定- 编译期开销和体积不可控
尤其在游戏 / 引擎 / 嵌入式中,这是禁区。
2⃣ 我们真正想要的是什么?
你要的是:
intid_int=get_id<int>();intid_double=get_id<double>();intid_int2=get_id<int>();assert(id_int==id_int2);即:
get_id ( T 1 ) = get_id ( T 2 ) ⟺ T 1 ≡ T 2 \text{get\_id}(T_1) = \text{get\_id}(T_2) \iff T_1 \equiv T_2get_id(T1)=get_id(T2)⟺T1≡T2
三、TypeId 技巧一:运行期递增 ID
1⃣ 全局计数器
externstd::atomic_int g_id_counter;inlineintgenerate_id(){returng_id_counter++;}2⃣ 模板绑定 ID
template<typenameT>intget_id(){staticconstintid=generate_id();returnid;}发生了什么?
- 每个
T有一个独立的 static - 第一次调用时生成 ID
- 后续调用复用
数学模型
id ( T ) = { new , 首次出现 cached , 之后 \text{id}(T) = \begin{cases} \text{new}, & \text{首次出现} \\ \text{cached}, & \text{之后} \end{cases}id(T)={new,cached,首次出现之后
3⃣ 致命缺陷
跨 DLL 失败
- 每个 DLL 有自己的
g_id_counter int在不同模块可能得到不同 ID
调用顺序依赖- 谁先
get_id<T>(),谁的编号小
不是 constexpr
四、TypeId 技巧二:constexpr 地址唯一性
这是现代 C++ 反射系统最常用的技巧之一。
1⃣ 预留模板静态变量
template<typename>booldummy_variable=false;- 每个
T都有独立实例 - 地址在整个程序中唯一(ODR)
2⃣ 用地址作为 TypeId
usingTemplateTypeId=void*;template<typenameT>constexprTemplateTypeIdget_id(){return&dummy_variable<T>;}3⃣ 为什么它成立?
C++ 规则保证:
T 1 ≠ T 2 ⇒ & d u m m y _ v a r i a b l e < T 1 > ≠ & d u m m y _ v a r i a b l e < T 2 > T_1 \neq T_2 \Rightarrow \&dummy\_variable<T_1> \neq \&dummy\_variable<T_2>T1=T2⇒&dummy_variable<T1>=&dummy_variable<T2>
而且:
- 不依赖初始化顺序
- 不需要运行期状态
- 可
constexpr - 零开销
仍然可能跨 DLL 失败(每个 DLL 有自己的模板实例)
五、在你的反射系统里的定位
现在你已经拥有:
Type*:完整结构描述TypeId:快速、轻量的类型识别- Constructor / Destructor:生命周期控制
这三者合在一起,你已经实现了:
Runtime Type System ≈ RTTI + Factory \text{Runtime Type System} \approx \text{RTTI} + \text{Factory}Runtime Type System≈RTTI+Factory
但: - 没有 RTTI
- 没有虚函数
- 完全由你控制
六、和 IFC / 模块反射的关系(非常关键)
你现在的体系正好适配 IFC:
- IFC → 提供类型结构
- 你 → 提供运行期注册 & 行为
- TypeId → 连接编译期与运行期
这是现实中 C++ 反射系统的终局形态。
七、一句话总结
Rapid fire additions 的本质是:
把“类型”从描述对象
升级为可构造、可销毁、可识别的实体
公式化总结:
Type : Metadata ⟶ Type : Runtime Object Model \text{Type} : \text{Metadata} \longrightarrow \text{Type} : \text{Runtime Object Model}Type:Metadata⟶Type:Runtime Object Model
一、什么是这里说的 “Base class slicing”?
这里的 slicing 不是“按值拷贝导致派生信息丢失”的经典 slicing,
而是更底层的含义:
当你拿着一个
void*(或T*)去当成基类用时,this指针是否指向正确的子对象起始地址?
举个反射中的真实问题
你在反射系统中通常只有:
void*object;Type*type;现在你知道:
object实际类型是C- 你想通过反射调用
BaseB::func
问题是:
BaseB子对象并不一定在C对象的起始地址
二、C++ 多继承的对象内存布局
structBaseA{inta;};structBaseB{intb;voidfunc();};structC:BaseA,BaseB{};一个典型布局(示意):
C object memory: +------------------+ | BaseA::a | offset 0 +------------------+ | BaseB::b | offset sizeof(BaseA) +------------------+也就是说:
$$
\text{addr}(BaseB\ subobject)
\text{addr}© + \text{offset}_{BaseB}
$$
关键结论
C*≠BaseB*- 但
static_cast<BaseB*>(C*)是合法且正确的 - 编译器知道
offset_{BaseB}
三、为什么反射系统一定要处理这个问题?
在反射系统中,你通常会做:
method.invoke(object_ptr,args);但:
method可能属于基类object_ptr指向派生类
如果你直接:
static_cast<BaseB*>(object_ptr);// object_ptr 是 void*你会得到:
错误的 this
未定义行为
神秘崩溃
四、Type 中为什么要记录 base class?
structType{std::string name;std::vector<int>base_class_type_ids;};这表示:
Type ( C ) → Type ( B a s e A ) , Type ( B a s e B ) \text{Type}(C) \rightarrow { \text{Type}(BaseA), \text{Type}(BaseB) }Type(C)→Type(BaseA),Type(BaseB)
反射系统因此知道:
- 这个类型有哪些基类
- 可以做:
- 向上转型
- 多态调用
- base method dispatch
五、核心原则:this 指针必须指向子对象起始
幻觉(错误的想法)
“Base 类就在对象开头吧?”
只对单继承 + 无虚继承成立
正确原则
任何时候,把对象当成 Base 使用前,都必须 rebase this 指针
数学形式:
this B a s e = rebase ∗ D e r i v e d → B a s e ( this ∗ D e r i v e d ) \text{this}_{Base} = \text{rebase}*{Derived \to Base}(\text{this}*{Derived})thisBase=rebase∗Derived→Base(this∗Derived)
六、rebase_ptr模板:把 this 指针交给编译器
template<typenameT,typenameTBase>void*rebase_ptr(void*object){T*typed_object=static_cast<T*>(object);returnstatic_cast<TBase*>(typed_object);}逐行解释
1⃣ 恢复派生类型
T*typed_object=static_cast<T*>(object);前提条件:
object真的是T实例- 由 Type registry 保证
2⃣ 关键操作:向上转型
static_cast<TBase*>(typed_object);这是本页最重要的一行。
编译器会:
- 查
T : TBase的继承关系 - 自动插入偏移
- 返回正确的子对象地址
数学表达
$$
\text{rebase_ptr}§
p + \Delta(T \rightarrow TBase)
$$
其中:
- Δ \DeltaΔ是编译期已知的偏移
- 对虚继承也成立(但可能更复杂)
七、为什么“让编译器处理(duplication)”是对的?
Slide 中写:
Can let the compiler deal with it (duplication)
意思是:
- 不要自己算 offset
- 不要解析 ABI
- 不要硬编码布局
而是:
用模板实例化,生成一份“从 T 到 Base 的专用转换函数”
这是最安全、最可移植、最符合 C++ 语义的方式。
八、在反射系统中的典型使用方式
你通常会在注册阶段生成:
Type base_type;base_type.rebase=&rebase_ptr<C,BaseB>;调用时:
void*base_object=type.rebase(object);method.invoke(base_object,args);九、常见误区总结
假设 base 在 offset 0
用reinterpret_cast
手写 offset
认为 slicing 只发生在值拷贝
十、一句话总结(非常重要)
Base class slicing 在反射里的真正含义是:
this 指针是否指向正确的子对象
最终原则:
Never guess object layout. Let the compiler rebase ‘this‘. \text{Never guess object layout. Let the compiler rebase `this`.}Never guess object layout. Let the compiler rebase ‘this‘.
一、未来最值得期待的事:绕过源代码解析器
1⃣ BMI / IFC 到底是什么?
你前面已经见过:
- MI:Module Interface(逻辑结构)
- BMI / IFC:编译器已经解析好的结果
这意味着:
IFC = 编译器 AST + 语义分析 + 名字查找的冻结产物
2⃣ “代码生成器可以直接去那里”的真正含义
Given BMI is a processed MI file, a code generator could directly go there, and forego the compiler’s source code parser entirely!
意思是:
- 不再:
- 写 Clang 插件
- 写 libclang
- 解析 C++ 语法
- 而是:
- 直接读取 BMI / IFC
- 从中提取:
- 类型
- 成员
- 继承
- 模板实例信息
数学化理解:
Source Code → Compiler IFC \text{Source Code} \xrightarrow{\text{Compiler}} \text{IFC}Source CodeCompilerIFC
而未来的反射工具:
IFC → Generator Reflection Data \text{IFC} \xrightarrow{\text{Generator}} \text{Reflection Data}IFCGeneratorReflection Data
跳过了最痛苦的一步:C++ 解析。
3⃣ 这为什么是“质变”?
- 不受宏 / include / 条件编译影响
- 不需要和编译器同步 C++ 语法
- 信息100% 与编译器一致
- 构建速度更快
这也是为什么:
Modules 是 tooling opportunity
二、Alternative Field De-virtualization:成员指针大小的坑
1⃣ 理想世界的想法
“既然所有
PtrToMember大小一样,那我就能直接存它,不用函数指针了。”
比如:
structField{std::string name;Type*type;PtrToMember member;};然后统一用:
object_ptr+member_offset2⃣ 现实世界(MSVC)
Slide 提到:
• PtrToMember = 4 bytes for POD
• 12 for forward decl
这暴露了一个ABI 层事实:
成员指针不是“偏移量”
在 MSVC 中:
- 对简单 POD + 单继承
- 成员指针 ≈ 偏移(4 bytes)
- 对:
- 多继承
- 虚继承
- incomplete type
- forward declaration
成员指针可能是:
{ offset, vbase_offset, flags }- 大小不固定
- 编译器私有表示
3⃣ 为什么这会直接否定这种方案?
你不能写:
std::byte buffer[sizeof(PtrToMember)];然后假设:
addr_field = addr_object + member \text{addr\_field} = \text{addr\_object} + \text{member}addr_field=addr_object+member
因为:
- member 可能不是 offset
- 甚至不是整数
4⃣ 所以函数指针方案为什么“丑但对”?
- 函数指针大小固定
- ABI 稳定
- 编译器负责解释
PtrToMember - 不需要知道内部布局
结论一句话:
成员指针不是数据,是“编译器协议”。
三、Alternative Method invoke:模板特化替代函数指针
现在来看最后这一部分代码。
1⃣ 核心思想
与其写:
AnyRef(*invoke)(void*,std::span<void*>);不如:
把 PtrToMemberFunction 直接作为模板参数
2⃣ InvokeHelper 的结构
template<autoPtrToMemberFunc>structInvokeHelper;这是一个主模板声明。
3⃣ 成员函数指针的偏特化
template<typenameTObject,typenameTReturn,typename...TArgs,TReturn(TObject::*PtrToMemberFunc)(TArgs...)>structInvokeHelper<PtrToMemberFunc>{staticAnyRefinvoke_func(void*object,std::span<void*>arguments){TObject*typed_object=static_cast<TObject*>(object);autoinvoke_internal=[&]<size_t...Indices>(std::index_sequence<Indices...>){return(typed_object->*PtrToMemberFunc)(*static_cast<TArgs*>(arguments[Indices])...);};returninvoke_internal(std::index_sequence_for<TArgs...>{});}};4⃣ 这个写法的本质优势
优点
- 完全无运行期模板信息
PtrToMemberFunc编译期已知- 强类型检查
- 更容易内联
InvokeHelper<&C::func>::invoke_func
数学抽象:
InvokeFunc f : ( void* , args ) → AnyRef \text{InvokeFunc}_{f} : (\text{void*}, \text{args}) \rightarrow \text{AnyRef}InvokeFuncf:(void*,args)→AnyRef
其中f ff是一个编译期常量成员函数指针。
代价
- 每个方法一个模板实例
- 代码体积可能变大
- 仍然绕不开成员函数指针 ABI
5⃣ 和之前函数指针方案的对比
| 方案 | 分发方式 | ABI 依赖 | 内联潜力 |
|---|---|---|---|
| virtual | vtable | 低 | |
| 函数指针 | indirect call | 低 | |
| InvokeHelper | 编译期实例 | 高 |
四、真正想传达的一句话
C++ 反射的极限不是“我能不能写出来”,
而是:我愿意在多大程度上相信编译器 ABI。
总结公式(终局视角)
$$
\text{Reflection} =
\text{Compiler Knowledge}
- \text{ABI Reality}
- \text{Engineering Trade-offs}
$$
五、把这些“未来想法”放回你的整体路线图
你已经完整走完了一条非常真实的路径:
- 手写反射数据结构
- 去虚函数
- 生命周期管理
- 类型识别
- 继承 / rebase
- ABI 边界探索
- 直指 BMI / IFC