news 2026/1/19 17:13:16

CppCon 2024 学习:Implementing Reflection Using the New C++20 Tooling Opportunity: Modules

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
CppCon 2024 学习:Implementing Reflection Using the New C++20 Tooling Opportunity: Modules

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 Query

3.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,类型是int
    • tag,类型是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,MemberInfoii=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}ObjectBytes / 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
    • 字段的静态类型信息
      重点
      这里完全没有写healthtag
      这段代码对所有结构体都成立

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:AnyJSON
递归定义:

  • 基础类型:直接映射
  • 复合类型:拆字段 → 对每个字段再调用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)xBuiltinxStruct
这个递归只有在“能枚举字段”时才可能存在

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}UIPropertyFieldInfo
    没有反射,就只能靠:
  • 代码生成
  • 手写 glue code

7.2 Language bindings(Python / Lua)

当你写:

entity.health=10

绑定层必须知道:

  • Entityhealth
  • 它的类型是什么
  • 如何读 / 写
    这本质是:
    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(Objectt1,Objectt2)
    前提依然是:

字段是可枚举的

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:TypeMetadata
      其中:
      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;}

这段代码在“设计上”想表达什么?

它假设了三件事:

  1. 对象是类型擦除的
    any any_value;
  2. 可以从对象直接反射出字段
    reflect_fields(any_value)
  3. 字段知道如何从对象中取值
    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,Fieldii=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:TypeIdType

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->fields
field.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:TypeNameType

  • 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,Fieldii=1n,Methodjj=1m)
它回答三类问题:

  1. 我叫什么?
  2. 我有哪些字段?
  3. 我有哪些方法?

2.3 为什么Type持有Field* / Method*

原因有三点:

  1. 类型擦除
    • Field/Method是多态基类
  2. 异构集合
    • 不同字段 / 方法的具体实现不同
  3. 运行时统一访问
    • 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 为什么argsvoid*

原因与字段一致:

  • 参数类型已通过parameter_types()描述
  • invoke只负责调度与调用
  • 类型检查由反射层或上层完成

6. Field / Method 是“类型系统的镜像”

可以这样理解:

C++ 静态世界RTTI 运行时世界
成员变量Field
成员函数Method
struct/classType
编译器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++ TypeRuntime 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::pfr
  • magic_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>Stuple<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 code

5.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}ParserBuild 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}ReflectionCompiler 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}HeaderPublic 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是什么?

.ifcC++ 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.qualifiedconst / volatile / ref 等
decl.structstruct/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 intint&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=SortNbitsIndex32Nbits

含义解释

  • 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_t
  • uint64_t→ 基本类型 partition
  • UUID→ 一个 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 + IFCRuntime 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++ 类型的运行期描述对象}TypeC++类型的运行期描述对象

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.lib

3⃣ 链接进最终程序

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++静态类型TTypeIDType对象
即:

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);

展开逻辑:

  1. 检查value_ptr.type == MyStruct
  2. 计算字段地址:
    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
  3. 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+UseInstantiation
而 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=1nComposable 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:TypeNameType
它是整个反射系统的“全局真相表(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>

原因通常有三种:

  1. 共享
    • 继承
    • 模板实例
  2. 稳定地址
    • Registry 重建
    • DLL 边界
  3. 避免拷贝
    从语义上:
    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}Typet0=Typet1
你就必须保证:

  • 所有指针仍然有效
  • 没有数据竞争

3⃣ name ≠ identity(重要)

理想设计通常是:

  • name:人类接口
  • TypeID:机器接口
    即:
    name → TypeID → Type \text{name} \rightarrow \text{TypeID} \rightarrow \text{Type}nameTypeIDType

八、一句话总结(架构层)

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;

编译器眼里的真实含义

这行代码在语义上等价于:

  1. 取得对象基地址:some_entity
  2. 取得成员nameEntity内的偏移
  3. 计算成员地址并访问
    用数学形式表示:
    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=entitymember_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 的问题

  1. 实现相关
    • 大小不固定
    • ABI 不稳定
  2. 不可序列化
  3. 跨 DLL 不可靠
  4. 无法动态生成

Relative offset 的优势


属性pointer-to-memberoffset
ABI 稳定X
可存储X
可生成X
来自 IFCX

六、Field abstraction = “反射级成员访问”

你可以把 Field 看成一个函数:
f : Object → Subobject f : \text{Object} \rightarrow \text{Subobject}f:ObjectSubobject
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}IFCFieldRuntime Access

一、这页真正想解决的是什么问题?

标题写得很直白:

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*}Objectvoid*

三、实现 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:Entityint

成员访问的展开

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);

这一步做的是:

  1. 使用成员指针定位字段
  2. 取字段地址
    数学表达:
    field_ptr = entity_ptr ∘ ptr_to_member \text{field\_ptr} = \text{entity\_ptr} \circ \text{ptr\_to\_member}field_ptr=entity_ptrptr_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⃣:虚函数 + 小对象,开销不低

每次字段访问:

  1. 虚函数分发
  2. 成员指针解引用
  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}} \rangleFieldImplTobject,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{可内联}(oPtrToMember);可内联
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(去虚函数)?

在前面的设计中,你经历了:

  1. Field是基类
  2. FieldImpl<TObject, TField>继承
  3. 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 PolymorphismCompile-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)T1T2

三、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 SystemRTTI+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:MetadataType: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=rebaseDerivedBase(thisDerived)

六、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_offset

2⃣ 现实世界(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 依赖内联潜力
virtualvtable
函数指针indirect call
InvokeHelper编译期实例

四、真正想传达的一句话

C++ 反射的极限不是“我能不能写出来”,
而是:我愿意在多大程度上相信编译器 ABI。

总结公式(终局视角)

$$
\text{Reflection} =
\text{Compiler Knowledge}

  • \text{ABI Reality}
  • \text{Engineering Trade-offs}
    $$

五、把这些“未来想法”放回你的整体路线图

你已经完整走完了一条非常真实的路径:

  1. 手写反射数据结构
  2. 去虚函数
  3. 生命周期管理
  4. 类型识别
  5. 继承 / rebase
  6. ABI 边界探索
  7. 直指 BMI / IFC
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/1/17 18:45:46

LobeChat能否对接Asana任务管理?项目协作智能化

LobeChat能否对接Asana任务管理&#xff1f;项目协作智能化 在远程办公常态化、跨职能协作日益频繁的今天&#xff0c;团队每天要面对大量分散的信息源&#xff1a;会议纪要藏在聊天记录里&#xff0c;待办事项写在白板上&#xff0c;关键决策埋没于邮件长河。一个常见的场景是…

作者头像 李华
网站建设 2026/1/18 13:29:18

重构开发链路:低代码如何成为企业数智化转型的关键抓手

目录 一、技术破局&#xff1a;低代码不是“简化开发”&#xff0c;而是“重构开发” 1. 组件化设计&#xff1a;从“重复造轮”到“模块复用” 2. 引擎化驱动&#xff1a;支撑复杂业务的“技术底座” 3. 可视化编排&#xff1a;打破“业务-IT”的沟通壁垒 二、成本重构&a…

作者头像 李华
网站建设 2026/1/18 12:05:42

使用PyTorch训练微调Qwen3-14B的入门级教程

使用PyTorch训练微调Qwen3-14B的入门级教程 在企业智能化转型加速的今天&#xff0c;越来越多公司希望部署具备领域理解能力的AI助手——既能读懂行业术语&#xff0c;又能联动内部系统自动执行任务。然而&#xff0c;通用大模型往往“懂语言但不懂业务”&#xff0c;而从零训练…

作者头像 李华
网站建设 2026/1/18 4:05:06

从代码看BuildingAI:企业级智能体平台设计解析

引言 近期&#xff0c; 在企业级开源智能体平台领域引起了开发者社区的关注。作为一名长期关注 AI 工程化落地的架构师&#xff0c;我决定深入其代码仓库&#xff08;GitHub/BidingCC/BuildingAI&#xff09;&#xff0c;从工程实现的角度进行一次系统性的技术分析。本文将以专…

作者头像 李华
网站建设 2026/1/17 13:18:05

负责处理大数据量的Excel导出功能

/*** 数据导出控制器* 负责处理大数据量的Excel导出功能*/ RestController RequestMapping("/api/export") public class ExportController {Autowiredprivate DataService dataService;/*** 内部类&#xff1a;Excel数据导出服务器* 实现EasyPOI的IExcelExportServe…

作者头像 李华
网站建设 2026/1/17 20:35:01

JMeter---正则表达式提取器

JMeter的正则表达式提取器是一个用于从服务器响应中提取特定数据的监听器。它可以根据正则表达式模式匹配响应内容&#xff0c;并提取匹配到的数据供后续测试步骤使用。 在JMeter的测试计划中选择需要提取数据的HTTP请求或其他请求&#xff0c;右键点击&#xff0c;选择"…

作者头像 李华