怎么建设一个开源平台网站,昆明平台网站开发,特价网站建设官网,河南网站建设公司价格1. What is reflection?#xff08;什么是反射#xff09;
1.1 直观定义
**反射#xff08;Reflection#xff09;**是指#xff1a; 程序在运行时#xff08;或编译期#xff09;能够“观察并操作自身结构”的能力 具体来说#xff0c;程序可以#xff1a; 知道某…1. What is reflection?什么是反射1.1 直观定义**反射Reflection**是指程序在运行时或编译期能够“观察并操作自身结构”的能力具体来说程序可以知道某个类型的名字枚举一个类型的成员变量 / 成员函数知道函数的参数类型、返回类型在运行时根据字符串或元信息调用函数、访问字段一句话概括Reflection Code → Metadata → Code1.2 非反射 vs 反射没有反射传统 CstructPerson{intage;std::string name;};Person p;p.age10;// 必须在编译期知道成员名你不能用字符串age访问p.age枚举Person的所有成员写一个“通用序列化器”而不手写代码有反射概念层面for(autofield:reflectPerson.fields()){print(field.name,field.get(p));}这类能力就是反射。1.3 编译期反射 vs 运行时反射类型特点编译期反射在编译时获取结构信息不引入运行时开销运行时反射元信息在运行时存在可动态查询C20 本身不提供标准反射但可以模拟运行时反射可以为未来的编译期反射std::meta, P1240做铺垫2. Why?为什么需要反射2.1 工程上的根本动机反射解决的是一个核心问题“如何让通用代码理解用户定义的类型”2.2 没有反射时的痛点1大量样板代码BoilerplatestructPerson{intage;std::string name;voidserialize(Jsonj)const{j[age]age;j[name]name;}};每个结构体都要手写字段一多就容易出错2通用算法失效你想写一个函数templatetypenameTvoidprint_object(constTobj);没有反射你根本不知道T有哪些字段字段叫什么如何访问2.3 反射能带来什么1通用序列化 / 反序列化to_json(obj);// 自动from_jsonT(j);// 自动2ORM / RPC / 自动绑定register_rpc_methodsMyService();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是字段名stringv i v_ivi是字段值any反射的本质把“类型”从一个不可观察的编译期实体映射成一个可枚举的结构集合3. Implementing runtime reflection如何实现运行时反射C20没有原生反射所以我们实现的是“约定式 / 模拟反射”3.1 基本思想核心策略把类型信息显式存储成数据结构即Type → Metadata → Runtime Query3.2 一个最小的反射系统组成1字段描述结构structFieldInfo{std::string_view name;std::functionvoid*(void*)get_ptr;};含义name字段名get_ptr(obj)返回字段地址2类型描述结构structTypeInfo{std::string_view name;std::vectorFieldInfofields;};3.3 为类型“注册”反射信息structPerson{intage;std::string name;};注册TypeInfo person_type{Person,{{age,[](void*obj){returnstatic_castPerson*(obj)-age;}},{name,[](void*obj){returnstatic_castPerson*(obj)-name;}}}};关键点使用void* lambda 做类型擦除成员访问被包装成函数对象3.4 使用反射Person p{18,Alice};for(autofield:person_type.fields){std::coutfield.name ;if(field.nameage){std::cout*static_castint*(field.get_ptr(p));}}3.5 抽象层级提升工程级真实项目中通常会使用宏减少样板代码使用模板自动生成TypeInfo使用std::any / std::variant承载值结合 RTTI / type_index例如#defineREFLECT_FIELD(type,field)\{#field,[](void*obj){returnstatic_casttype*(obj)-field;}}3.6 运行时反射的代价优点灵活易与脚本 / 配置系统结合缺点性能开销间接调用类型安全弱void*编译期错误变成运行时错误3.7 与未来 C 编译期反射的关系未来提案如std::meta目标是constexprautoinforeflexpr(Person);forconstexpr(automember:info.members()){// 编译期展开}这意味着Reflection → constexpr Zero Runtime Cost \text{Reflection} \xrightarrow{\text{constexpr}} \text{Zero Runtime Cost}ReflectionconstexprZero Runtime Cost4. 总结Summary4.1 一句话总结反射让程序“理解自己”从而写出真正通用的代码4.2 在 C20 中的现实结论没有标准反射可以用模板 数据结构模拟运行时反射需要权衡性能、类型安全与工程复杂度为未来 C 编译期反射做好设计准备1. Reflection 的核心定义1.1 一句话定义反射Reflection就是把“代码本身的信息”当作“数据”来使用。换句话说Reflection Metadata of code1.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 没有反射时的状态关键在 C20 中Entity e;e.health10;你不能写出for(automember:e.members()){...}// 不存在原因是成员信息在编译完成后被“抹掉”了3. 用数学抽象理解“成员枚举”我们可以把一个类型T TT抽象为一个集合T m 1 , m 2 , … , m n T { m_1, m_2, \dots, m_n }Tm1,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} }Entityhealth: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:reflectEntity.members()){print(member.name,member.type);}即从类型反推出成员5. 成员为什么必须是“元数据”5.1 如果成员不是元数据会怎样假设你想写一个通用打印函数templatetypenameTvoiddump(constTobj);如果没有成员的元数据编译器无法展开程序无法枚举你只能为每个类型单独写版本5.2 有反射时的理想模型反射系统内部通常等价于TypeInfo ( T ) ( name , MemberInfo ∗ i ∗ i 1 n ) \text{TypeInfo}(T) \left( \text{name},\\ {\text{MemberInfo}*i}*{i1}^{n} \right)TypeInfo(T)(name,MemberInfo∗i∗i1n)其中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∗(baseptroffset)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 所以 C20 的现实是编译器知道所有成员程序拿不到这些信息需要手工或宏“重新描述一遍结构”8. 小总结针对你这页内容核心要点回顾Reflection code 的 metadata问题“What members do I have?”本质是类型结构是否可被程序观察对Entity而言成员变量 成员函数构成其结构数学抽象类型是成员的集合反射就是暴露这个集合1. Why should I care about reflection?一句话先给结论因为反射让“通用程序”成为可能而序列化是第一个、也是最典型的例子。2. Serialization为什么序列化离不开反射2.1 什么是 Serialization序列化序列化就是把内存中的对象转换成一种可存储 / 可传输的表示Binary二进制JSONXMLProtobuf …抽象地说Object ⟶ Bytes / Text \text{Object} \longrightarrow \text{Bytes / Text}Object⟶Bytes / Text2.2 没有反射时的序列化困境假设你有structEntity{inthealth;std::string tag;};传统 C 写法voidto_json(jsonj,constEntitye){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()字段当前的值仍然是anyf.type字段的静态类型信息重点这里完全没有写health、tag这段代码对所有结构体都成立4. 递归序列化真正的威力4.1 顶层分发逻辑你给出的第二段代码本质是一个通用 dispatcherjsonvalue_to_json(any any_value){// Handle builtinsif(any_value.type()the_typeint())returnjson{any_value.valueint()};if(any_value.type()the_typedouble())returnjson{any_value.valuedouble()};if(any_value.type()the_typestd::string())returnjson{any_value.valuestd::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递归定义基础类型直接映射复合类型拆字段 → 对每个字段再调用serialize4.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 code7.2 Language bindingsPython / Lua当你写entity.health10绑定层必须知道Entity有health它的类型是什么如何读 / 写这本质是Dynamic Language → Reflection Static C \text{Dynamic Language} \xrightarrow{\text{Reflection}} \text{Static C}Dynamic LanguageReflectionStatic C7.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 一句话总结反射让类型从“只属于编译器”变成“也属于程序本身”如果你愿意下一步我可以用C20 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}Metadataname,members,attributes,…2.2 在 C 中的现实限制类型信息主要存在于编译器内部运行时只保留最少的 RTTI成员列表、字段名、参数名统统不可见结论反射必须“显式构建”而不是“自动获得”3. Go over current techniques现有实现反射的技术路线下面是 C 社区目前真实在用的几种路线。3.1 RTTI最弱但原生typeid(T).name();dynamic_castBase*(ptr);能力类型身份identity继承关系有限不能做的枚举成员访问字段调用任意函数RTTI ≠ Reflection3.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现代 C20 风格核心思想把“反射信息”变成编译期常量templatetypenameTstructreflect;templatestructreflectEntity{staticconstexprautofieldsstd::make_tuple(fieldEntity::health(health),fieldEntity::tag(tag));};数学抽象reflect T ∈ constexpr \text{reflect}T \in \text{constexpr}reflectT∈constexpr优点零运行时开销类型安全缺点需要显式列字段写法偏复杂3.5 外部代码生成Codegen流程C Source ↓ (parser) Metadata ↓ (generator) C Reflection Code例子protobufUnreal Header ToolQt 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 ASTStable 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_ptroffset);即field_value ∗ ( p Δ ) \text{field\_value} *(p \Delta)field_value∗(pΔ)这是所有运行时字段访问的基础。5.3 Visitor / Dispatcher 模式visit(value,[](autov){// 根据真实类型分发});用于序列化打印比较5.4 注册表Type Registryunordered_mapTypeId,TypeInforegistry;作用类型 → 元数据动态查找插件系统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 API7. 总结这一页你真正要传达的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但在 C20 中并不现实。2. Go from realistic client API走向现实可行的 API2.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::anystd::any不暴露内部布局无法枚举字段只能做“类型判断 强转”而反射需要类型 → 字段 → 偏移 / 访问器4. 用 Type 驱动反射而不是对象4.1 现实版的序列化函数jsonserialize_struct(AnyRef any_value){json json;for(constFieldf: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) 在做什么逻辑等价于autobasestatic_castchar*(any_value.value);autofield_ptrbaseoffset;returnAnyRef{field_ptr,field_type};数学抽象field_addr object_addr Δ \text{field\_addr} \text{object\_addr} \Deltafield_addrobject_addrΔ其中Δ \DeltaΔ是字段在对象中的偏移量。6. Type类型元数据的核心6.1 Type 的结构structType{std::string_view name;std::vectorFieldfields;};语义上Type ( name , Field ∗ i ∗ i 1 n ) \text{Type} \left( \text{name},\\ {\text{Field}*i}*{i1}^{n} \right)Type(name,Field∗i∗i1n)6.2 为什么字段列表属于 Type因为字段是类型级信息同一个类型的所有对象字段布局一致对象只需要携带一个Type*这使得Memory overhead per object O ( 1 ) \text{Memory overhead per object} O(1)Memory overhead per objectO(1)7. Type Registry类型注册表7.1 为什么需要 Registry你必须回答这个问题“给我一个对象地址我怎么知道它的 Type 是什么”答案必须有一个全局或模块级的 Type Registry7.2 Registry 的基本结构unordered_mapTypeId,Typeregistry;或概念化为Registry : TypeId → Type \text{Registry} : \text{TypeId} \rightarrow \text{Type}Registry:TypeId→Type7.3 注册流程构造阶段register_typeEntity({Entity,{{health,int_type,make_fieldEntity::health()},{tag,string_type,make_fieldEntity::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)完全不知道offsetpointer arithmeticregistry 实现模板 / 宏这正是一个成功的 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()AnyRef10. 一句话总结RTTI runtime 的本质是把“类型结构”从编译器手里搬到程序手里。1. Implementing type registry实现类型注册表1.1 为什么需要 Type Registry反射运行时必须能回答一个问题“系统里有哪些类型给我一个名字我能拿到它的结构吗”因此需要一个全局的类型存储中心。1.2 最小形式的 Type Registryexternstd::unordered_mapstd::string,Typetype_registry;语义上这是一个映射TypeRegistry : TypeName → Type \text{TypeRegistry} : \text{TypeName} \rightarrow \text{Type}TypeRegistry:TypeName→Typekey类型名字符串value类型的完整元数据1.3 类型注册 APItemplatetypenameTvoidregister_type(Field[],Method[]);这一步在程序初始化阶段完成把编译期类型T转换成运行时Type对象放入 registry2. Defining Type定义 Type2.1 Type 的角色structType{std::string name;std::vectorField*fields;std::vectorMethod*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}*{i1}^{n},\ {\text{Method}*j}*{j1}^{m} \right)Type(name,Field∗i∗i1n,Method∗j∗j1m)它回答三类问题我叫什么我有哪些字段我有哪些方法2.3 为什么Type持有Field* / Method*原因有三点类型擦除Field/Method是多态基类异构集合不同字段 / 方法的具体实现不同运行时统一访问Type只关心接口不关心实现3. Defining Field interface字段反射接口这一页是整个运行时反射的关键设计点之一3.1 Field 的目标把“成员变量”变成一个可以运行时操作的对象3.2 为什么需要“类型擦除”成员变量的类型是无限多的inthealth;std::string tag;floatposition[3];你不可能写std::vectorFieldint...std::vectorFieldstd::string...所以必须抹掉具体类型只保留行为4. 1st instinct虚函数基类你给出的设计是最自然、也是最经典的方案classField{public:virtual~Field()default;virtualstd::string_viewname()0;virtualconstType*type()0;virtualAnyRefvalue(void*object)0;};4.1 每个接口函数在干什么1name()virtualstd::string_viewname()0;返回字段名用于JSON key编辑器 UI日志2type()virtualconstType*type()0;返回字段的类型元数据注意不是 C 类型而是反射系统的 Type数学表示Field → type() Type \text{Field} \xrightarrow{\text{type()}} \text{Type}Fieldtype()Type3value(void* object)virtualAnyRefvalue(void*object)0;这是最重要的接口。语义是给我一个对象地址返回这个字段对应的值抽象为value : ( Object ∗ , Field ) → AnyRef \text{value} : (\text{Object}^*, \text{Field}) \rightarrow \text{AnyRef}value:(Object∗,Field)→AnyRef4.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::spanconstTypeparameter_types()0;virtualAnyRefinvoke(void*object,std::spanvoid*args)0;};5.2 Method 接口的语义分解1name()方法名用于脚本绑定RPCUI 按钮2return_type()virtualconstType*return_type()0;数学表示Method → return_type() Type \text{Method} \xrightarrow{\text{return\_type()}} \text{Type}Methodreturn_type()Type3parameter_types()virtualstd::spanconstTypeparameter_types()0;返回参数类型列表顺序即调用顺序抽象为Method → ( Type 1 , … , Type n ) \text{Method} \rightarrow (\text{Type}_1, \dots, \text{Type}_n)Method→(Type1,…,Typen)4invoke(...)virtualAnyRefinvoke(void*object,std::spanvoid*args)0;语义是在运行时用一组“无类型参数”调用该方法数学模型invoke : ( Object ∗ , Args ∗ ) → AnyRef \text{invoke} : (\text{Object}^*, \text{Args}^*) \rightarrow \text{AnyRef}invoke:(Object∗,Args∗)→AnyRef5.3 为什么args是void*原因与字段一致参数类型已通过parameter_types()描述invoke只负责调度与调用类型检查由反射层或上层完成6. Field / Method 是“类型系统的镜像”可以这样理解C 静态世界RTTI 运行时世界成员变量Field成员函数Methodstruct/classType编译器Type Registry7. 设计代价与收益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 dataGathering 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 Type1.2 对外 Public API极其重要你反复强调的 API.type-fields field.value()这是刻意设计的结果客户端代码永远不关心类型如何注册字段如何生成用的是宏 / 模板 / parser1.3 注册入口函数templatetypenameTvoidregister_type(Field[],Method[]);这一行是编译期世界 → 运行时世界的“桥”。2. Current techniques: Manual手动方式2.1 代表RTTRstructMyStruct{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 ASTHumanMetadata2.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 #1std::tuple_element技巧4.1 代表库boost::pfrmagic_get4.2 原理利用聚合初始化规则structS{inta;doubleb;};编译器允许auto[x,y]s;于是可以推导S ≡ tuple i n t , d o u b l e S \equiv \text{tuple}int, doubleS≡tupleint,double4.3 能做到什么字段数量字段类型字段顺序4.4 做不到什么关键字段名成员函数私有成员属性 / 注解所以你总结得非常准确Not flexible enough5. Automatic #2Code Parser代码解析器5.1 代表方案QtMeta Object Compiler (MOC)Unreal EngineUnreal Header Tool (UHT)5.2 思路C Source ↓ (parser) AST ↓ (generator) Reflection code5.3 问题 1C 语法极端复杂模板宏条件编译重载 / ADL解析“完整 C”几乎等于“写一个编译器”5.4 问题 2维护成本灾难你原文说得非常直白Maintenance is a mess原因标准更新频繁各家编译器行为不同用户代码风格多样5.5 问题 3构建系统深度绑定必须处理include 路径宏定义编译选项平台差异数学上可以理解为Parser ⊆ Build System \text{Parser} \subseteq \text{Build System}Parser⊆Build System6. Automatic #3使用编译器前端LibClang6.1 思路升级既然 C 太难解析那我直接用编译器6.2 工具LibClangClangTooling6.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 processC 编译流程我们先把你给出的流程整理成一条标准管线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 的工程实践中.hstruct / class成员变量成员函数声明模板定义.cpp函数实现算法逻辑私有细节也就是说几乎所有“反射关心的信息”都集中在头文件里2.2 用反射视角重新理解头文件传统理解.h是“为了让别的文件能编译”反射视角.h是类型系统的公开接口描述可以抽象为Header ≈ Public Type Metadata \text{Header} \approx \text{Public Type Metadata}Header≈Public Type Metadata3. 编译器在中间到底做了什么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或使用 LibClang5.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_typeEntity(...);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 Types8. 为什么这件事这么“别扭”因为 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}ModulesBetter Compiler ArtifactReflection Feature三、Modules 带来的真实变化1⃣ 模块接口先被单独编译Module interfaces .ixx / .cppm ↓ Compiler ↓ Binary Module Interface (BMI) .ifc / .pcm / .gcm这是Modules 与传统头文件的根本差异。2⃣ 什么是 Binary Module InterfaceBMIBMI 不是.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 neededBMI 本身就是完整类型上下文。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/.gcm2⃣ BMI ≠ 反射输入格式问题现实BMI 是否标准否可否跨编译器否可否被用户代码访问否是否稳定否结论BMI 是给编译器用的不是给程序或工具用的七、Separate target separation boundary致命现实这是构建系统层面的硬边界。1⃣ 构建系统看到的世界Target A: Modules → BMI Target B: Sources → .obj中间没有“中途钩子”没有“编译中回调”没有“消费 BMI 再生成代码”的标准步骤2⃣ 你图中绿色区域在表达什么你画的MyProject-ReflectionDataBMI ↓ Reflection Generator外部工具 ↓ Generated .cpp ↓ Compiler本质是反射生成被迫退化为“一个独立工程 / 独立 target”八、这意味着什么1⃣ Modules 没有失败Modules成功地集中类型信息消除头文件解析提升编译性能2⃣ 但 Modules 不是反射方案关键等式Modules Reflection Enabler \text{Modules} \text{Reflection Enabler}ModulesReflection Enabler但Modules ≠ Reflection Mechanism \text{Modules} \neq \text{Reflection Mechanism}ModulesReflection Mechanism九、真正缺失的那一块缺的不是“类型信息”而是语言级、标准化的“编译期语义访问能力”也就是后来大家在做的Reflection TSstd::meta编译期 AST 视图十、一句话总结这一页非常重要Modules 让反射在工程上第一次“看起来合理”但它依然无法让反射在语言层面“真正发生”。一、.ifc是什么.ifc是C Modules 的二进制接口文件Binary Module Interface, BMI在MSVC体系中叫IFCInterface 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“Mapstr, Partition*”这是非常准确的理解。Header 做什么Header 是MapString,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/classdecl.function函数expr表达式stmt语句Partition 内部是“数组式存储”五、Qualified Type 的内部结构你给的 Figure 9.16你给了这段关键描述E.g. partition“type.qualified” QualifiedType[N];unqualified: TypeIndexqualifiers: 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_uTuunqualified type例如intQ 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}AbstractIndexSort∗Nbits∥Index∗32−Nbits含义解释Sorttag→ 表示“这是哪一类实体”typedeclexprIndex→ 在对应 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.modulepartition2⃣struct UUIDDecl.Struct ├─ name → UUID ├─ fields: │ └─ uuid : uint64_tuint64_t→ 基本类型 partitionUUID→ 一个 Decl.Struct Index3⃣struct Entity字段会被表示为health → TypeIndex(int) name → TypeIndex(std::string) uuid → TypeIndex(UUID) x,y,z → TypeIndex(float)注意UUID并不是展开结构而是通过 AbstractIndex 引用另一个 struct九、Inspecting.ifcIfc SDK Visualizer你提到Inspecting .ifc• Ifc sdk visualizer这是 MSVC 官方工具作用是把.ifc可视化成PartitionDecl 树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*typeNeat::get_typeMyStruct();也就是说Type ≈ C 类型的运行期描述对象 \text{Type} \approx \text{C 类型的运行期描述对象}Type≈C类型的运行期描述对象2⃣ generates data neatly listening to MSVC这句话的关键在listening。Neat 并不是“重新解析 C”而是监听MSVC 编译模块读取.ifcBinary Module Interface从中提取structfieldtypelayoutoffset也就是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*typeNeat::get_typeMyStruct();这里发生的是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 模型反射的核心抽象TypestructType{TypeID id;std::vectorFieldfields;};它表示Type f 0 , f 1 , … , f n \text{Type} { f_0, f_1, \dots, f_n }Typef0,f1,…,fnFieldstructField{std::string name;TypeID type;size_t offset;};字段在对象中的地址计算公式是$$\text{field_address}\text{object_address} \text{offset}$$六、关键操作通过反射修改字段1⃣ 拿到字段constFieldfieldtype-fields[0];即fields[0]→damage2⃣ 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.offsetaddrvalue_ptr.addressfield.offset将75写入该地址等价于*(int*)((char*)valueoffset)75;但带有类型系统约束。3⃣ 验证结果assert(value.damage75);这说明反射并不是“复制”数据而是直接操作对象内存七、为什么这是“安全的”反射与传统 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 猜测直接来自.ifc3⃣ 无宏、无侵入exportstructMyStruct{...};不需要REFLECT()REGISTER_TYPE()X-Macro八、这套方案在 C 生态中的定位可以这样理解方案特点宏反射侵入性强constexpr反射编译期不能跨 DLLClang 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 dumpModules 的核心承诺是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}IFCClang AST with plugins对反射库的影响你不能依赖属性做标记只能用命名约定module / partition 边界类型结构本身这迫使库设计变成“结构驱动反射”而不是“注解驱动反射”3⃣ BMI filename queryBMI 文件名不可查询现象你不知道import X;对应的.ifc文件路径原因这是刻意的设计选择BMI 是编译器内部缓存不是用户接口文件名、位置、缓存策略都是编译器私有可随版本变化对反射工具的影响你的工具必须通过编译器调用链而不是猜路径扫磁盘二、Non-trivial limitations真正“硬”的限制这些限制才是会影响反射能力边界的。1⃣ Templates not instantiated模板未实例化现象templatetypenameTstructBox{T value;};在 IFC 中只有模板定义没有BoxintBoxfloat原因语言层面模板实例化规则是Template Use ⟹ Instantiation \text{Template} \text{Use} \Longrightarrow \text{Instantiation}TemplateUse⟹Instantiation而 IFC 只描述接口潜在语义它不承担“猜测你将来会用什么 T”对运行期反射的影响这意味着IFC 不能直接告诉你“所有类型”你的库只能反射非模板类型或已具体化的类型2⃣ Compiler specific编译器特定现象.ifcMSVC.pcmClang.gcmGCC结构、语义、索引规则都不同。原因现实C 没有标准化的 BMI 格式各家编译器用 modules但不共享内部表示对库作者的含义你的库实际上是Reflection Library Compiler Backend \text{Reflection Library} \text{Compiler Backend}Reflection LibraryCompiler Backend而不是一个单一实现。3⃣ IPRIntermediate 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 Objects2⃣ During registration you have the type这是非常关键的一句。含义在生成反射数据时你已经知道类型字段布局offset因此不需要 RTTI hack不需要typeid不需要dynamic_cast3⃣ Design of library meant to be rebuild using library pieces这句话体现的是成熟库设计思维。含义你的反射库不是一个 monolith而是Type systemRegistryAnyPtrField opsBackend (IFC)可以表示为Reflection System ∑ i 1 n Composable Components \text{Reflection System} \sum_{i1}^{n} \text{Composable Components}Reflection Systemi1∑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_mapstd::string,Typetype_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作为 keystd::unordered_mapstd::string,Type含义是类型名是人类可理解的入口可用于调试序列化脚本绑定编辑器数学抽象name ∈ Σ ∗ ⇒ Type \text{name} \in \Sigma^* \Rightarrow \text{Type}name∈Σ∗⇒Type三、register_type() 在“注册”什么你给出的是templatetypenameTvoidregister_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_typeTyperuntime2⃣ register_type 内部逻辑概念版templatetypenameTvoidregister_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::vectorField*fields;std::vectorMethod*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 }Ff1,f2,…,fn字段集合M m 1 , m 2 , … , m k M { m_1, m_2, \dots, m_k }Mm1,m2,…,mk方法集合2⃣ Field / Method 是“可共享的描述对象”你用的是std::vectorField*std::vectorMethod*这隐含了一个重要设计点Field / Method 不是 Type 的子对象而是独立实体3⃣ 为什么不用vectorField原因通常有三种共享继承模板实例稳定地址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 通常还会有什么虽然你没贴出来但在这种系统里几乎必然包含FieldstructField{std::string name;TypeID type;size_t offset;};字段地址计算公式$$\text{addr}_{field}\text{addr}_{object} \text{offset}$$MethodstructMethod{std::string name;TypeID return_type;std::vectorTypeIDparams;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⃣ 通过名字查类型Typettype_registry[MyStruct];2⃣ 遍历字段编辑器 / 序列化for(Field*f:t.fields){// UI / JSON / Binary}3⃣ 方法调用脚本 / RPCMethod*mt.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∗t0Type∗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_entityGetSomeEntity();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_memberGetSomeMember();Entity entity;std::coutentity.*some_member;这里发生了什么some_member不再是名字而是一个值value它描述了如何从一个Entity得到某个成员这正是 C 的 pointer-to-memberstd::string Entity::*ptrEntity::name;entity.*ptr;语义上value entity ∘ member_descriptor \text{value} \text{entity} \circ \text{member\_descriptor}valueentity∘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_castchar*(object)offset;}数学形式f ( o ) o offset f f(o) o \text{offset}_ff(o)ooffsetf五、为什么不用 C 原生 pointer-to-member这是一个反射系统必须面对的问题。原生 pointer-to-member 的问题实现相关大小不固定ABI 不稳定不可序列化跨 DLL 不可靠无法动态生成Relative offset 的优势属性pointer-to-memberoffsetABI 稳定X√可存储X√可生成X√来自 IFCX√六、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 accessobjectoffset八、和你整个 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一、这页真正想解决的是什么问题标题写得很直白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*三、实现 FieldFieldImpl 的核心思想你给出的实现是classFieldImpl:publicField{public:usingPtrToMemberintEntity::*;PtrToMember ptr_to_member;AnyRefvalue(void*object)override{Entity*entity_objectstatic_castEntity*(object);int*field_ptr(entity_object-*ptr_to_member);returnAnyRef{field_ptr,int_type};}};这段代码非常“教科书级”我们逐层拆。四、PtrToMemberC 原生的“成员描述符”usingPtrToMemberintEntity::*;这不是普通指针而是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_objectstatic_castEntity*(object);把类型擦除的 object 恢复成具体类型这是一个前提假设调用者保证object确实指向一个Entityint*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_ptrentity_ptr∘ptr_to_memberreturnAnyRef{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 强绑定到 EntityusingPtrToMemberintEntity::*;意味着一个 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_addrobject_addroffset无 ABI 风险可序列化来自 IFC不依赖Entity::*九、这页在整个反射设计中的地位你这页的意义不是“最终方案”而是说明“最自然的 C 面向对象设计”为什么在反射系统中会遇到边界。它起到的是思维过渡设计对比教育读者“为什么需要更底层的抽象”十、一句话总结非常重要这页展示的是如何用 C 语言自身的机制virtual pointer-to-member第一次触碰“运行期字段访问”这个问题而后续的设计正是对它的“工程化升级”。公式版总结Field : void* → AnyRef \text{Field} : \text{void*} \rightarrow \text{AnyRef}Field:void*→AnyRef一、第一版模板化 FieldImpl泛化成员类型代码templatetypenameTObject,typenameTFieldclassFieldImpl:publicField{public:usingPtrToMemberTField TObject::*;// DataPtrToMember ptr_to_member;// FunctionsAnyRefvalue(void*object)override{TObject*typed_objectstatic_castTObject*(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_castTObject*(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把“成员指针”变成编译期常量代码templatetypenameTObject,typenameTField,TField TObject::*PtrToMemberclassFieldImpl: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_objectstatic_castTObject*(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_addrobject_addr???八、两条必然的出路这正是你后面会讲的出路 1relative offset你前面已经铺垫structField{std::string name;Type*type;size_t offset;};AnyRefField::value(void*object){returnAnyRef{static_castchar*(object)offset,type};}数学表达f ( o ) o offset f(o) o \text{offset}f(o)ooffset这是IFC 驱动反射的最终形态。出路 2函数指针 / lambda不如 offsetusingAccessFnAnyRef(*)(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是基类FieldImplTObject, 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;// FunctionsusingValueFuncAnyRef(*)(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编译期绑定运行期调用模板实现templatetypenameTObject,typenameTField,TField TObject::*PtrToMemberAnyRefvalue_func(void*object){TObject*typed_objectstatic_castTObject*(object);TField*field_ptr(typed_object-*PtrToMember);returnAnyRef{field_ptr,field_type};}这段代码在“反射层面”做了什么1⃣ 类型恢复唯一的危险点TObject*typed_objectstatic_castTObject*(object);这是约定型安全registry 保证Field只用于对应TObject2⃣ 成员访问的本质(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::vectorType*method_parameter_types;// FunctionsusingInvokeFuncAnyRef(*)(void*,std::spanvoid*);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反射调用的核心算法模板定义templateautoPtrToMemberFunction,typenameTObject,typenameTReturn,typename...TParamsAnyRefinvoke_func(void*object,std::spanvoid*arguments)1⃣ 编译期安全校验static_assert(std::is_same_vdecltype(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_objectstatic_castTObject*(object);与 Field 一样这是约定型安全。3⃣ 参数展开最精彩的部分autoinvoke_internal[]size_t...Indices(std::index_sequenceIndices...){return(typed_object-*PtrToMemberFunction)(*static_castTParams*(arguments[Indices])...);};这里发生了什么arguments是void*arguments[N]TParams...是(P0,P1,...,PN)展开后等价于typed_object-method(*static_castP0*(arguments[0]),*static_castP1*(arguments[1]),...);数学表示$$m(o, a_0, \dots, a_n)(o \rightarrow^* f)(*a_0, \dots, *a_n)$$4⃣ 返回值处理returninvoke_internal(std::index_sequence_forTParams...{});如果TReturn是值类型你通常会拷贝到 storage再返回AnyRefslide 中为简化省略了这一层六、这一设计相比虚函数方案的优势性能层面项目虚函数函数指针分发成本高低可内联部分对 cache一般更友好架构层面Field / Method 是POD-like易于存 registry跨模块序列化行为由模板生成逻辑集中七、但这仍然不是“最终形态”你应该已经注意到了Field 仍依赖PtrToMemberMethod 仍依赖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::vectorField*fields;std::vectorMethod*methods;// FunctionsusingConstructorFuncvoid(*)(void*);ConstructorFunc constructor;usingDestructorFuncvoid(*)(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⃣ 类型擦除的构造函数templatetypenameTvoiderased_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⃣ 类型擦除的析构函数templatetypenameTvoiderased_destructor(void*object){static_castT*(object)-~T();}重要细节只调用析构不释放内存内存管理策略由上层决定destruct T ( p ) : p → ∼ T ( ) \text{destruct}_T(p) : p \rightarrow \sim T()destructT(p):p→∼T()5⃣ 为什么这套设计很“C”无虚函数无 RTTI生命周期完全由反射系统接管非常适合GameDevEmbedded自定义 allocator二、TypeId不用 RTTI 的“类型识别”1⃣ 为什么不能用typeid(T)虽然 C 提供typeid(T)但在很多工程里RTTI 被关闭-fno-rttitypeid跨 DLL / SO 行为不稳定编译期开销和体积不可控尤其在游戏 / 引擎 / 嵌入式中这是禁区。2⃣ 我们真正想要的是什么你要的是intid_intget_idint();intid_doubleget_iddouble();intid_int2get_idint();assert(id_intid_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 技巧一运行期递增 ID1⃣ 全局计数器externstd::atomic_int g_id_counter;inlineintgenerate_id(){returng_id_counter;}2⃣ 模板绑定 IDtemplatetypenameTintget_id(){staticconstintidgenerate_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_counterint在不同模块可能得到不同 ID调用顺序依赖谁先get_idT()谁的编号小不是 constexpr四、TypeId 技巧二constexpr 地址唯一性这是现代 C 反射系统最常用的技巧之一。1⃣ 预留模板静态变量templatetypenamebooldummy_variablefalse;每个T都有独立实例地址在整个程序中唯一ODR2⃣ 用地址作为 TypeIdusingTemplateTypeIdvoid*;templatetypenameTconstexprTemplateTypeIdget_id(){returndummy_variableT;}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\_variableT_1 \neq \dummy\_variableT_2T1T2⇒dummy_variableT1dummy_variableT2而且不依赖初始化顺序不需要运行期状态可constexpr零开销仍然可能跨 DLL 失败每个 DLL 有自己的模板实例五、在你的反射系统里的定位现在你已经拥有Type*完整结构描述TypeId快速、轻量的类型识别Constructor / Destructor生命周期控制这三者合在一起你已经实现了Runtime Type System ≈ RTTI Factory \text{Runtime Type System} \approx \text{RTTI} \text{Factory}Runtime Type System≈RTTIFactory但没有 RTTI没有虚函数完全由你控制六、和 IFC / 模块反射的关系非常关键你现在的体系正好适配 IFCIFC → 提供类型结构你 → 提供运行期注册 行为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_castBaseB*(C*)是合法且正确的编译器知道offset_{BaseB}三、为什么反射系统一定要处理这个问题在反射系统中你通常会做method.invoke(object_ptr,args);但method可能属于基类object_ptr指向派生类如果你直接static_castBaseB*(object_ptr);// object_ptr 是 void*你会得到错误的 this未定义行为神秘崩溃四、Type 中为什么要记录 base classstructType{std::string name;std::vectorintbase_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})thisBaserebase∗Derived→Base(this∗Derived)六、rebase_ptr模板把 this 指针交给编译器templatetypenameT,typenameTBasevoid*rebase_ptr(void*object){T*typed_objectstatic_castT*(object);returnstatic_castTBase*(typed_object);}逐行解释1⃣ 恢复派生类型T*typed_objectstatic_castT*(object);前提条件object真的是T实例由 Type registry 保证2⃣ 关键操作向上转型static_castTBase*(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.rebaserebase_ptrC,BaseB;调用时void*base_objecttype.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 到底是什么你前面已经见过MIModule 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_ptrmember_offset2⃣ 现实世界MSVCSlide 提到• PtrToMember 4 bytes for POD• 12 for forward decl这暴露了一个ABI 层事实成员指针不是“偏移量”在 MSVC 中对简单 POD 单继承成员指针 ≈ 偏移4 bytes对多继承虚继承incomplete typeforward 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_fieldaddr_objectmember因为member 可能不是 offset甚至不是整数4⃣ 所以函数指针方案为什么“丑但对”函数指针大小固定ABI 稳定编译器负责解释PtrToMember不需要知道内部布局结论一句话成员指针不是数据是“编译器协议”。三、Alternative Method invoke模板特化替代函数指针现在来看最后这一部分代码。1⃣ 核心思想与其写AnyRef(*invoke)(void*,std::spanvoid*);不如把 PtrToMemberFunction 直接作为模板参数2⃣ InvokeHelper 的结构templateautoPtrToMemberFuncstructInvokeHelper;这是一个主模板声明。3⃣ 成员函数指针的偏特化templatetypenameTObject,typenameTReturn,typename...TArgs,TReturn(TObject::*PtrToMemberFunc)(TArgs...)structInvokeHelperPtrToMemberFunc{staticAnyRefinvoke_func(void*object,std::spanvoid*arguments){TObject*typed_objectstatic_castTObject*(object);autoinvoke_internal[]size_t...Indices(std::index_sequenceIndices...){return(typed_object-*PtrToMemberFunc)(*static_castTArgs*(arguments[Indices])...);};returninvoke_internal(std::index_sequence_forTArgs...{});}};4⃣ 这个写法的本质优势优点完全无运行期模板信息PtrToMemberFunc编译期已知强类型检查更容易内联InvokeHelperC::func::invoke_func数学抽象InvokeFunc f : ( void* , args ) → AnyRef \text{InvokeFunc}_{f} : (\text{void*}, \text{args}) \rightarrow \text{AnyRef}InvokeFuncf:(void*,args)→AnyRef其中f ff是一个编译期常量成员函数指针。代价每个方法一个模板实例代码体积可能变大仍然绕不开成员函数指针 ABI5⃣ 和之前函数指针方案的对比方案分发方式ABI 依赖内联潜力virtualvtable低函数指针indirect call低InvokeHelper编译期实例高四、真正想传达的一句话C 反射的极限不是“我能不能写出来”而是我愿意在多大程度上相信编译器 ABI。总结公式终局视角$$\text{Reflection} \text{Compiler Knowledge}\text{ABI Reality}\text{Engineering Trade-offs}$$五、把这些“未来想法”放回你的整体路线图你已经完整走完了一条非常真实的路径手写反射数据结构去虚函数生命周期管理类型识别继承 / rebaseABI 边界探索直指 BMI / IFC