1. What is reflection?(什么是反射)

1.1 直观定义

**反射(Reflection)**是指:

程序在运行时(或编译期)能够“观察并操作自身结构”的能力
具体来说,程序可以:

  • 知道某个类型的 名字
  • 枚举一个类型的 成员变量 / 成员函数
  • 知道函数的 参数类型、返回类型
  • 在运行时 根据字符串或元信息调用函数、访问字段
    一句话概括:

Reflection = Code → Metadata → Code

1.2 非反射 vs 反射

没有反射(传统 C++)
struct Person {
    int age;
    std::string name;
};
Person p;
p.age = 10;      // 必须在编译期知道成员名

不能

  • 用字符串 "age" 访问 p.age
  • 枚举 Person 的所有成员
  • 写一个“通用序列化器”而不手写代码
有反射(概念层面)
for (auto field : 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)
struct Person {
    int age;
    std::string name;
    void serialize(Json& j) const {
        j["age"] = age;
        j["name"] = name;
    }
};
  • 每个结构体都要手写
  • 字段一多就容易出错
(2)通用算法失效

你想写一个函数:

template<typename T>
void print_object(const T& 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_i ni 是字段名(string)
  • v i v_i vi 是字段值(any)
    反射的本质:

把“类型”从一个 不可观察的编译期实体
映射成一个 可枚举的结构集合

3. Implementing runtime reflection(如何实现运行时反射)

C++20 没有原生反射
所以我们实现的是:“约定式 / 模拟反射”

3.1 基本思想

核心策略:

把类型信息显式存储成数据结构
即:

Type → Metadata → Runtime Query

3.2 一个最小的反射系统组成

(1)字段描述结构
struct FieldInfo {
    std::string_view name;
    std::function<void*(void*)> get_ptr;
};

含义:

  • name:字段名
  • get_ptr(obj):返回字段地址
(2)类型描述结构
struct TypeInfo {
    std::string_view name;
    std::vector<FieldInfo> fields;
};

3.3 为类型“注册”反射信息

struct Person {
    int age;
    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
    例如:
#define REFLECT_FIELD(type, field) \
{ #field, [](void* obj) { return &static_cast<type*>(obj)->field; } }

3.6 运行时反射的代价

优点
  • 灵活
  • 易与脚本 / 配置系统结合
缺点
  • 性能开销(间接调用)
  • 类型安全弱(void*
  • 编译期错误变成运行时错误

3.7 与未来 C++ 编译期反射的关系

未来提案(如 std::meta)目标是:

constexpr auto info = reflexpr(Person);
for constexpr (auto member : info.members()) {
    // 编译期展开
}

这意味着:
Reflection → constexpr Zero Runtime Cost \text{Reflection} \xrightarrow{\text{constexpr}} \text{Zero Runtime Cost} Reflectionconstexpr Zero 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 为例

struct Entity
{
    int health;
    std::string tag;
    void eat_burger();
};

反射视角,我们关心的不是“如何用它”,而是:

  • 类型名:Entity
  • 数据成员:
    • health,类型是 int
    • tag,类型是 std::string
  • 成员函数:
    • eat_burger(),返回 void,无参数
      这些信息在普通 C++ 里:
  • 编译器知道
  • 程序运行时不知道

2.3 没有反射时的状态(关键)

在 C++20 中:

Entity e;
e.health = 10;

不能 写出:

for (auto member : e.members()) { ... } // 不存在

原因是:

成员信息在编译完成后被“抹掉”了

3. 用数学抽象理解“成员枚举”

我们可以把一个类型 T T T 抽象为一个集合:
T = m 1 , m 2 , … , m n T = { m_1, m_2, \dots, m_n } T=m1,m2,,mn
其中:

  • m i m_i mi 表示一个 成员(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 T T 显式地暴露出来

4. Reflection 在这里到底“反”了什么

4.1 正常编程方向

Type definition → Use members

你先写结构体,然后在代码里硬编码地使用成员名

4.2 有反射的方向

Type definition → Metadata → Generic code

反射允许你:

for (auto member : reflect<Entity>.members()) {
    print(member.name, member.type);
}

即:从类型反推出成员

5. 成员为什么必须是“元数据”

5.1 如果成员不是元数据会怎样?

假设你想写一个通用打印函数:

template<typename T>
void dump(const T& 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 数据成员

int health;
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 成员函数

void eat_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 没有反射时的序列化困境

假设你有:

struct Entity {
    int health;
    std::string tag;
};

传统 C++ 写法:

void to_json(json& j, const Entity& 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 你给出的核心伪代码(逐行理解)

json serialize_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;
    }
    return json;
}
这段代码在“反射世界”里意味着什么?
  • any any_value
    • 类型被擦除了
    • 但反射系统能恢复结构信息
  • reflect_fields(any_value)
    • 返回:这个对象“有哪些字段”
    • 这是反射提供的能力
  • f.name
    • 字段名(字符串)
  • f.value()
    • 字段当前的值(仍然是 any
  • f.type
    • 字段的静态类型信息
      重点
      这里完全没有写 healthtag
      这段代码对 所有结构体都成立

4. 递归序列化:真正的威力

4.1 顶层分发逻辑

你给出的第二段代码本质是一个 通用 dispatcher

json value_to_json(any any_value)
{
    // Handle builtins
    if (any_value.type() == the_type<int>())
        return json{ any_value.value<int>() };
    if (any_value.type() == the_type<double>())
        return json{ any_value.value<double>() };
    if (any_value.type() == the_type<std::string>())
        return json{ any_value.value<std::string>() };
    // Recurse for classes
    if (any_value.is_class()) {
        return serialize_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 LanguageReflection Static 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(最直接)

struct FieldInfo {
    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 宏(现实中最常见)

struct Entity {
    REFLECT()
    int health;
    std::string tag;
};

宏展开后:

  • 生成字段列表
  • 生成 TypeInfo
    优点:
  • 使用成本低
  • 工程可行
    缺点:
  • 宏不可调试
  • 破坏语义清晰度

3.4 模板 + constexpr(现代 C++20 风格)

核心思想:

把“反射信息”变成编译期常量

template<typename T>
struct reflect;
template<>
struct reflect<Entity> {
    static constexpr auto fields = 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(类型擦除)

struct Any {
    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])
int health;

用于:

  • 编辑器
  • 校验
  • 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 / Codegen MetadataRuntime Registry Reflection API

7. 总结(这一页你真正要传达的)

7.1 关键结论

  • C++ 反射不是“等语言给”
  • 而是:
    • 模板
    • Modules
    • 工程模式
      的组合拳

7.2 一句话收尾

反射不是某一个特性,而是一整套设计方法。

1. Implementing an RTTI runtime(实现 RTTI 运行时)

1.1 从“理想客户端 API”开始

你最开始给出的接口是:

json serialize_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;
    }
    return json;
}

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

它假设了三件事:

  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:运行时对象的最小表示

struct AnyRef
{
    void* value;
    const Type* 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 现实版的序列化函数

json serialize_struct(AnyRef any_value)
{
    json json;
    for (const Field& f : any_value.type->fields) {
        json[f.name]["value"] = f.value(any_value);
        json[f.name]["type"] = f.type;
    }
    return json;
}

关键变化只有一个:

any_value.type->fields

这意味着:

字段信息来自“类型”,而不是对象本身

5. Field:字段到底是什么?

5.1 Field 的抽象定义

struct Field {
    std::string_view name;
    const Type* type;
    Value (*value)(AnyRef);
};
  • name:字段名
  • type:字段类型
  • value()从任意对象中取出该字段的值

5.2 field.value(any_value) 在做什么?

逻辑等价于:

auto base = static_cast<char*>(any_value.value);
auto field_ptr = base + offset;
return AnyRef{ field_ptr, field_type };

数学抽象:
field_addr = object_addr + Δ \text{field\_addr} = \text{object\_addr} + \Delta field_addr=object_addr+Δ
其中 Δ \Delta Δ 是字段在对象中的偏移量。

6. Type:类型元数据的核心

6.1 Type 的结构

struct Type {
    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 客户端与实现解耦

序列化代码:

json serialize_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} Objectwrap AnyRefType* Typefields Fieldvalue() AnyRef

10. 一句话总结

RTTI runtime 的本质,是把“类型结构”从编译器手里,搬到程序手里。

1. Implementing type registry(实现类型注册表)

1.1 为什么需要 Type Registry

反射运行时必须能回答一个问题:

“系统里有哪些类型?给我一个名字,我能拿到它的结构吗?”
因此需要一个全局的类型存储中心

1.2 最小形式的 Type Registry

extern std::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<typename T>
void register_type(Field[], Method[]);

这一步在程序初始化阶段完成:

  • 编译期类型 T
  • 转换成 运行时 Type 对象
  • 放入 registry

2. Defining Type(定义 Type)

2.1 Type 的角色

struct Type
{
    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 为什么需要“类型擦除”

成员变量的类型是无限多的:

int health;
std::string tag;
float position[3];

你不可能写:

std::vector<Field<int>> ...
std::vector<Field<std::string>> ...

所以必须:

抹掉具体类型,只保留行为

4. 1st instinct:虚函数基类

你给出的设计是最自然、也是最经典的方案

class Field
{
public:
    virtual ~Field() = default;
    virtual std::string_view name() = 0;
    virtual const Type* type() = 0;
    virtual AnyRef value(void* object) = 0;
};

4.1 每个接口函数在干什么?

(1)name()
virtual std::string_view name() = 0;
  • 返回字段名
  • 用于:
    • JSON key
    • 编辑器 UI
    • 日志
(2)type()
virtual const Type* type() = 0;
  • 返回字段的类型元数据
  • 注意:不是 C++ 类型,而是 反射系统的 Type
    数学表示:
    Field → type() Type \text{Field} \xrightarrow{\text{type()}} \text{Type} Fieldtype() Type
(3)value(void* object)
virtual AnyRef value(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 的设计

class Method
{
public:
    virtual ~Method() = default;
    virtual std::string_view name() = 0;
    virtual const Type* return_type() = 0;
    virtual std::span<const Type> parameter_types() = 0;
    virtual AnyRef invoke(void* object, std::span<void*> args) = 0;
};

5.2 Method 接口的语义分解

(1)name()
  • 方法名
  • 用于:
    • 脚本绑定
    • RPC
    • UI 按钮
(2)return_type()
virtual const Type* return_type() = 0;

数学表示:
Method → return_type() Type \text{Method} \xrightarrow{\text{return\_type()}} \text{Type} Methodreturn_type() Type

(3)parameter_types()
virtual std::span<const Type> parameter_types() = 0;
  • 返回参数类型列表
  • 顺序即调用顺序
    抽象为:
    Method → ( Type 1 , … , Type n ) \text{Method} \rightarrow (\text{Type}_1, \dots, \text{Type}_n) Method(Type1,,Typen)
(4)invoke(...)
virtual AnyRef invoke(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 / class Type
编译器 Type Registry

7. 设计代价与收益

7.1 代价

  • 虚函数调用(间接调用)
  • 内存占用
  • 初始化顺序管理

7.2 收益

  • 真正的运行时反射
  • 动态语言 / 编辑器 / RPC 友好
  • 客户端 API 极简、稳定

8. 一句话总结(这一页的核心)

Type Registry + Field / Method 接口 = 一个可运行的“迷你类型系统”。
如果你愿意,下一步我可以:

  • 展示 FieldImpl / MethodImpl 如何用模板生成
  • 或分析 不用虚函数、改用 function pointer / constexpr table 的替代设计

Gathering type data → Manual → Automatic(多种),并在最后给一个整体对照与结论

1. Gathering type data

Gathering type data is the meat and potatoes of our reflection library
因为:

  • Registry / Field / Method 只是“容器”
  • 真正困难的是:如何获得这些信息

1.1 Type Registry 的角色回顾

Type registry:
  Contains type definitions

本质映射:
TypeRegistry : C++ Type → Runtime Type \text{TypeRegistry} : \text{C++ Type} \rightarrow \text{Runtime Type} TypeRegistry:C++ TypeRuntime Type

1.2 对外 Public API(极其重要)

你反复强调的 API:

.type->fields
field.value()

这是刻意设计的结果:

  • 客户端代码 永远不关心
    • 类型如何注册
    • 字段如何生成
    • 用的是宏 / 模板 / parser

1.3 注册入口函数

template<typename T>
void register_type(Field[], Method[]);

这一行是编译期世界 → 运行时世界的“桥”。

2. Current techniques: Manual(手动方式)

2.1 代表:RTTR

struct MyStruct { 
    MyStruct() {}; 
    void func(double) {}; 
    int data; 
};
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++ ASTHuman Metadata

2.3 优点

  • 稳定
  • 可控
  • 不依赖工具链
  • 易于调试

2.4 致命问题

(1)重复定义

int data;          // 定义一次
.property("data") // 再写一次

DRY 原则被破坏

(2)维护成本指数级增长

如果:

  • 类型数量 = N N N
  • 字段数量 = M M M
    维护成本近似为:
    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 原理

利用聚合初始化规则:

struct S { int a; double b; };

编译器允许:

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)} ASTCompile Binary (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.cpp
static bool registered = [] {
    register_type<Entity>(...);
    return true;
}();

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 TypesExtraction MetadataInitialization Runtime 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 InterfaceCompile BMIReflect Metadata 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.qualified const / volatile / ref 等
decl.struct struct/class
decl.function 函数
expr 表达式
stmt 语句
Partition 内部是“数组式存储”

五、Qualified Type 的内部结构(你给的 Figure 9.16)

你给了这段关键描述:

E.g. partition
“type.qualified”
= QualifiedType[N];
unqualified: TypeIndex
qualifiers: Qualifiers
我们把它还原成结构体:

struct QualifiedType {
    TypeIndex unqualified;   // 去掉修饰后的类型
    Qualifiers qualifiers;   // const / volatile / ref / ...
};

用数学形式表示

一个 QualifiedType 可以表示为:
T q = ⟨ T u ,   Q ⟩ T_q = \langle T_u,\ Q \rangle Tq=Tu, Q
其中:

  • T u T_u Tu:unqualified type(例如 int
  • Q Q Q:限定符集合(例如 {const, &}

示例

const int&

.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= SortN bits  Index32N bits

含义解释

  • Sort(tag)
    → 表示“这是哪一类实体”
    • type
    • decl
    • expr
  • Index
    → 在对应 Partition 中的数组下标

直观理解

(AbstractIndex)
= “第 3 类 Partition 中的第 128 号对象”

编译器看到一个 Index,不需要猜:

  • 类型是什么?
  • 存在哪?

一跳就到目标语义对象

七、String Table:去重 + 快速比较

.ifc 中:

  • 所有字符串(标识符、命名空间名)
  • 都只存一次
  • 用整数索引引用
    例如:
"Entity" → StringID 42
"name"   → StringID 57

优点:

  • 比较字符串 = 比较整数
  • 省空间
  • 快速反序列化

八、示例:EntityModule.ixx 如何进入 .ifc

源码

export module EntityModule;
export struct UUID {
    uint64_t uuid;
};
export struct Entity {
    int health;
    std::string name;
    UUID uuid;
    float x, y, z;
};

.ifc 中会发生什么?

1⃣ Module 信息
  • module 名:EntityModule
  • 存在 decl.module partition
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
    你最终拿到的是:
const Type* 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 Generator Reflection Data (.cpp/.obj)

3⃣ integrates into your CMake project

核心是这行:

add_reflection_target(MyProject_ReflectionData MyProject)

它的语义是:

“基于 MyProject 生成一个伴生的反射数据目标”

三、构建期发生了什么(非常重要)

1⃣ 模块代码

export struct MyStruct {
    int damage;
};

这一步:

  • 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⃣ 获取类型对象

const Type* 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 的本质是:

struct AnyPtr {
    void* address;
    TypeID type;
};

数学上可以看作:
AnyPtr = ⟨ 地址 ,  类型标识 ⟩ \text{AnyPtr} = \langle \text{地址},\ \text{类型标识} \rangle AnyPtr=地址, 类型标识
它解决了一个核心问题:

“我有一块内存,但我不知道它的静态类型”

五、Type / Field 模型(反射的核心抽象)

Type

struct Type {
    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

struct Field {
    std::string name;
    TypeID type;
    size_t offset;
};

字段在对象中的地址计算公式是:
$$
\text{field_address}

\text{object_address} + \text{offset}
$$

六、关键操作:通过反射修改字段

1⃣ 拿到字段

const Field& 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.offset addr=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⃣ 字段偏移来自编译器

  • 不靠 offsetof hack
  • 不靠 ABI 猜测
  • 直接来自 .ifc

3⃣ 无宏、无侵入

export struct MyStruct { ... };

不需要:

  • 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)Neat Runtime 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)]]
struct A {};

→ 在 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<typename T>
struct Box { 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;materialize Runtime 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(类型注册表) 的职责。
你给出的设计:

extern std::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<typename T>
void register_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_type Typeruntime

2⃣ register_type 内部逻辑(概念版)

template<typename T>
void register_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 的关系模型

你给出的结构是:

struct Field;
struct Method;
struct Type {
    std::string name;
    std::vector<Field*> fields;
    std::vector<Method*> methods;
};

我们把它提升到抽象层。

1⃣ Type 是“中心节点”

数学上:
Type = ⟨ name ,   F , M ⟩ \text{Type} = \langle \text{name},\ F, M \rangle Type=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

struct Field {
    std::string name;
    TypeID type;
    size_t offset;
};

字段地址计算公式:
$$
\text{addr}_{field}

\text{addr}_{object} + \text{offset}
$$

Method

struct Method {
    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 R m:(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 TruthCodegen Type / Field / MethodRegistry Runtime 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
意思是:
不是“对象 → 成员”,而是“成员 → 对象”

把成员变成变量

auto some_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*
  • 而是:
    • 相对于对象起始地址的成员位置

用结构体说明

struct Entity {
    int health;
    std::string name;
    float x, 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} \rangle Field=offset, type
访问公式:
$$
\text{field_addr}

\text{object_addr} + \text{offset}
$$

四、Field 抽象:把“成员”变成数据

反射系统里的 Field,本质上就是:

“可运行期使用的 pointer-to-member”

一个典型的 Field 定义

struct Field {
    std::string name;
    TypeID type;
    size_t offset;
};

这里的 offset

  • 就是 relative pointer
  • &Entity::name 在语义上等价
  • 但:
    • 可序列化
    • 可存储
    • 可跨模块

Field 的访问逻辑

void* Field::get_address(void* object) const {
    return static_cast<char*>(object) + offset;
}

数学形式:
f ( o ) = o + offset f f(o) = o + \text{offset}_f f(o)=o+offsetf

五、为什么不用 C++ 原生 pointer-to-member?

这是一个反射系统必须面对的问题

原生 pointer-to-member 的问题

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

Relative offset 的优势


属性 pointer-to-member offset
ABI 稳定 X
可存储 X
可生成 X
来自 IFC X

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

你可以把 Field 看成一个函数:
f : Object → Subobject f : \text{Object} \rightarrow \text{Subobject} f:ObjectSubobject
set_value / get_value 是:
f ( o ) : = v f(o) := v f(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 of T;;Field (runtime value)

二、第一反应:基类 + 虚函数(非常自然)

你写了:

class Field
{
public:
    virtual ~Field() = default;
    virtual std::string_view name() = 0; 
    virtual const Type* type() = 0;
    virtual AnyRef value(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 的核心思想

你给出的实现是:

class FieldImpl : public Field
{
public:
    using PtrToMember = int Entity::*;
    PtrToMember ptr_to_member;
    AnyRef value(void* object) override
    {
        Entity* entity_object = static_cast<Entity*>(object);
        int* field_ptr = &(entity_object->*ptr_to_member);
        return AnyRef{ field_ptr, int_type };
    }
};

这段代码非常“教科书级”,我们逐层拆。

四、PtrToMember:C++ 原生的“成员描述符”

using PtrToMember = int Entity::*;

这不是普通指针,而是 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
return AnyRef{ field_ptr, int_type };

这一步是运行期反射的“交付时刻”
AnyRef = ⟨ address ,  Type ⟩ \text{AnyRef} = \langle \text{address},\ \text{Type} \rangle AnyRef=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

using PtrToMember = int Entity::*;

意味着:

  • 一个 FieldImpl
  • 只能服务一个具体 Entity
  • 一个字段类型 = 一个类
    FieldImpl 数量爆炸

问题 2⃣:pointer-to-member 的 ABI 问题

  • 大小不固定
  • 编译器实现相关
  • 多继承 / 虚继承更复杂
    这在 跨 DLL / 跨编译器 的反射系统里是个隐患。

问题 3⃣:虚函数 + 小对象,开销不低

每次字段访问:

  1. 虚函数分发
  2. 成员指针解引用
  3. AnyRef 构造
    对 editor / tools OK
    对 hot loop

八、为什么你前面强调 “relative pointer”

结合你前一页 slide,其实你自己已经在“推翻”这个方案了 😄
更稳定的模型是:

struct Field {
    size_t offset;
    const Type* 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<typename TObject, typename TField>
class FieldImpl : public Field
{
public:
    using PtrToMember = TField TObject::*;
    // Data
    PtrToMember ptr_to_member;
    // Functions
    AnyRef value(void* object) override
    {
        TObject* typed_object = static_cast<TObject*>(object);
        TField* field_ptr = &(typed_object->*ptr_to_member);
        return AnyRef{ field_ptr, field_type };
    }
};

这一步解决了什么?

相比最早写死 Entity / int 的版本,这一版完成了:

字段“类型 + 所属对象类型”的完全泛化
数学上:
FieldImpl ⟨ T object , T field ⟩ \text{FieldImpl}\langle T_{\text{object}}, T_{\text{field}} \rangle FieldImplTobject,Tfield
表示的是一个函数:
f ( o ) = address of  ( o . T field ) f(o) = \text{address of } (o.T_{\text{field}}) f(o)=address of (o.Tfield)

类型擦除在哪里?

  • Field 接口:
    AnyRef value(void* object)
    
  • 类型恢复发生在这里:
    static_cast<TObject*>(object)
    

也就是说:
void* → static_cast T object ∗ \text{void*} \xrightarrow{\text{static\_cast}} T_{\text{object}}^* void*static_cast Tobject

前提仍然是:调用者必须传对对象类型。

二、Finished Field Implementation:补齐“反射语义”

新增内容

std::string field_name;
Type* field_type;

以及:

std::string_view name() override { return field_name; }
const Type* type() override { return field_type; }

这一版的意义

这一步非常重要,因为它标志着:

FieldImpl 不再只是“访问器”,而是“反射对象”
现在一个 Field 完整描述了:
Field = ⟨ name ,  type ,  accessor ⟩ \text{Field} = \langle \text{name},\ \text{type},\ \text{accessor} \rangle Field=name, type, accessor
这正是反射系统中“字段”的最小语义闭包。

三、Simplifying Field:把“成员指针”变成编译期常量

代码

template<typename TObject, typename TField, TField TObject::* PtrToMember>
class FieldImpl : public Field
{
public:
    // Data
    std::string field_name;
    Type* field_type;
    // Functions
    std::string_view name() override { return field_name; }
    const Type* type() override { return field_type; }
    AnyRef value(void* object) override
    {
        TObject* typed_object = static_cast<TObject*>(object);
        TField* field_ptr = &(typed_object->*PtrToMember);
        return AnyRef{ 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 变成“纯数据结构”

你给出的目标形态

struct Field
{
    // Data
    std::string name;
    Type* type;
    // Functions
    AnyRef value(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(你前面已经铺垫)

struct Field {
    std::string name;
    Type* type;
    size_t offset;
};
AnyRef Field::value(void* object)
{
    return AnyRef{
        static_cast<char*>(object) + offset,
        type
    };
}

数学表达:
f ( o ) = o + offset f(o) = o + \text{offset} f(o)=o+offset
这是 IFC 驱动反射 的最终形态。

出路 2:函数指针 / lambda(不如 offset)

using AccessFn = AnyRef(*)(void*);
AccessFn access;

但:

  • 有间接调用
  • 不能序列化
  • ABI 更复杂

九、这几页真正想传达的“设计结论”

你这一连串 slide 在讲的不是代码,而是一个结论:

C++ 语言级抽象(继承 / 模板 / pointer-to-member)
最终会逼你回到“偏移 + 类型”这一底层模型。

公式版总结:
Field = ⟨ name ,  type ,  offset ⟩ \text{Field} = \langle \text{name},\ \text{type},\ \text{offset} \rangle Field=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 结构

struct Field
{
    // Data
    std::string name;
    Type* type;
    // Functions
    using ValueFunc = 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 \rangle Field=name, type, f
其中:
f ( o ) = AnyRef f(o) = \text{AnyRef} f(o)=AnyRef

三、Field 的 value_func:编译期绑定,运行期调用

模板实现

template<typename TObject, typename TField, TField TObject::* PtrToMember>
AnyRef value_func(void* object)
{
    TObject* typed_object = static_cast<TObject*>(object);
    TField* field_ptr = &(typed_object->*PtrToMember);
    return AnyRef{ 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} \rangle AnyRef=address, Type

关键设计点总结(Field)

  • 模板决定一切
  • 运行期只做一次函数指针调用
  • 没有虚函数
  • 没有 RTTI
  • 没有 dynamic_cast

四、De-virtualized Method:把“成员函数调用”也变成函数指针

1⃣ Method 结构

struct Method
{
    // Data
    std::string method_name;
    Type* method_return_type;
    std::vector<Type*> method_parameter_types;
    // Functions
    using InvokeFunc = 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<auto PtrToMemberFunction,
         typename TObject,
         typename TReturn,
         typename... TParams>
AnyRef invoke_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 TReturn PtrToMemberFunction:(TObject::)(TParams...)TReturn
如果模板参数不一致,直接编译失败

2⃣ 类型恢复

TObject* typed_object = static_cast<TObject*>(object);

与 Field 一样,这是约定型安全。

3⃣ 参数展开(最精彩的部分)

auto invoke_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⃣ 返回值处理

return invoke_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 结构

struct Type
{
    // Data
    std::string name;
    std::vector<Field*> fields;
    std::vector<Method*> methods;
    // Functions
    using ConstructorFunc = void (*)(void*);
    ConstructorFunc constructor;
    using DestructorFunc = void (*)(void*);
    DestructorFunc destructor;
};

抽象意义

Type 现在不仅描述结构,还描述生命周期:
Type = ⟨ name ,  fields ,  methods , construct ,  destruct ⟩ \text{Type} = \langle \text{name},\ \text{fields},\ \text{methods}, \text{construct},\ \text{destruct} \rangle Type=name, fields, methods,construct, destruct

3⃣ 类型擦除的构造函数

template<typename T>
void erased_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<typename T>
void erased_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⃣ 我们真正想要的是什么?

你要的是:

int id_int  = get_id<int>();
int id_double = get_id<double>();
int id_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_2 get_id(T1)=get_id(T2)T1T2

三、TypeId 技巧一:运行期递增 ID

1⃣ 全局计数器

extern std::atomic_int g_id_counter;
inline int generate_id()
{
    return g_id_counter++;
}

2⃣ 模板绑定 ID

template<typename T>
int get_id()
{
    static const int id = generate_id();
    return id;
}

发生了什么?

  • 每个 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>
bool dummy_variable = false;
  • 每个 T 都有 独立实例
  • 地址在整个程序中唯一(ODR)

2⃣ 用地址作为 TypeId

using TemplateTypeId = void*;
template<typename T>
constexpr TemplateTypeId get_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++ 多继承的对象内存布局

struct BaseA { int a; };
struct BaseB { int b; void func(); };
struct C : 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?

struct Type
{
    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<typename T, typename TBase>
void* rebase_ptr(void* object)
{
    T* typed_object = static_cast<T*>(object);
    return static_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 CodeCompiler IFC
        而未来的反射工具:
        IFC → Generator Reflection Data \text{IFC} \xrightarrow{\text{Generator}} \text{Reflection Data} IFCGenerator Reflection Data
        跳过了最痛苦的一步:C++ 解析。

3⃣ 这为什么是“质变”?

  • 不受宏 / include / 条件编译影响
  • 不需要和编译器同步 C++ 语法
  • 信息100% 与编译器一致
  • 构建速度更快
    这也是为什么:

Modules 是 tooling opportunity

二、Alternative Field De-virtualization:成员指针大小的坑

1⃣ 理想世界的想法

“既然所有 PtrToMember 大小一样,那我就能直接存它,不用函数指针了。”
比如:

struct Field
{
    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<auto PtrToMemberFunc>
struct InvokeHelper;

这是一个 主模板声明

3⃣ 成员函数指针的偏特化

template<
    typename TObject,
    typename TReturn,
    typename... TArgs,
    TReturn (TObject::*PtrToMemberFunc)(TArgs...)
>
struct InvokeHelper<PtrToMemberFunc>
{
    static AnyRef invoke_func(void* object, std::span<void*> arguments)
    {
        TObject* typed_object = static_cast<TObject*>(object);
        auto invoke_internal = [&]<size_t... Indices>(
            std::index_sequence<Indices...>)
        {
            return (typed_object->*PtrToMemberFunc)(
                *static_cast<TArgs*>(arguments[Indices])...
            );
        };
        return invoke_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 f f 是一个编译期常量成员函数指针

代价

  • 每个方法一个模板实例
  • 代码体积可能变大
  • 仍然绕不开成员函数指针 ABI

5⃣ 和之前函数指针方案的对比

方案 分发方式 ABI 依赖 内联潜力
virtual vtable
函数指针 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
Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐