CppCon 2017 学习:Meta

背景(Background)

  • 和 Herb Sutter 合作开发 metaclasses(元类)提案
  • 目标是利用元类和反射机制,支持更好的网络编程工具
  • 统一用 C++ 来定义元信息和代码生成规范,单一规范,不依赖其他语言
  • 用元编程自动生成:
    • 更安全的解码器(decoders)
    • 更高效的解码器
    • 实用的编码器(encoders),比如支持模糊测试(fuzz testing)

概览(Overview)

  • 这是对之前"反射"话题的延续,目的是进一步讲解:
    • 如何将源代码"表示"为编译时数据(静态反射)
    • 如何从这些数据"产生"源代码(代码生成)
  • 本质上是可编程的代码生成
    • 即写程序去操作代码的结构数据,再输出代码文本或可用代码

理解总结

这部分讲的是用反射和元类机制,把源代码当成数据来读写和生成,从而实现:

  • 自动化地生成复杂且安全的网络协议处理代码
  • 利用 C++ 的静态反射和元编程机制,做到"代码即数据,数据即代码"的编程范式
  • 支持更灵活、高效、可维护的代码生成流程
    这不仅是"查看代码结构",更是"用代码结构生成代码",即 静态代码生成的高阶设计思想

这部分内容讲了 实现元类(metaclasses)所需要的要素、挑战,以及元程序(metaprograms)的角色和设计思路。我帮你总结理解:

Metaclass Requirements (元类的需求)

要实现元类功能,需要能够:

  • 写和执行"元程序"(metaprogram)------ 在编译期间运行的程序,可以读取和写入代码结构
  • 读取类的属性和成员------ 反射机制支持查看类的组成部分
  • 创建新的类来接收"输出" ------ 相当于基于已有类生成新的类定义
  • 复制原始类的成员到新类中
  • 生成新的声明插入到输出类
  • 最终输出(emit)新生成的类

Other Use Cases (其他应用场景)

元类不仅仅是简单地从旧类生成新类:

  • 生成非成员函数(类接口的一部分)
  • 在其他命名空间中做特化(例如 std::hash 的特化)
  • 包装类和函数,加入额外代码(类似装饰器)
  • 精准控制代码生成的位置和方式
    换言之,元类和元程序用于各种复杂的代码注入和生成需求

Approach (实现思路)

  • 充分考虑不同的实际用例
  • 定义具体的操作符(operators),只做"正好需要的事",不要过度泛化语法
  • 先设计语义(功能)再考虑语法美化,当前的语法可能不好看但要能用
  • 元类的注入操作必须直接建立在反射系统之上,不要搞两个完全分离的系统

Metaprograms (元程序)

  • 元程序是能把源代码当数据读取和生成代码的程序

  • 只对"声明"(declarations)有效

  • 模板元编程是一种特例,用模板机制编码值和生成代码(实例化)

  • 需要一个更通用、可编程的生成系统,不局限于模板

  • 支持在编译时运行的程序块(constexpr块) ,比如:

    cpp 复制代码
    constexpr {
      for (auto x : $x.member_variables())
        // 对成员做处理
    }
  • 元程序可以出现在命名空间、类、函数等各种作用域中

  • 思路参考了 Daveed Vandevoorde 的 Metacode 和提案 P0633

总结

元类的实现依赖一个强大的静态反射+元程序系统,这个系统能让你在编译期写程序操作代码结构,并生成新的代码。设计上要注重实际功能需求,避免语法过度复杂,同时保证反射和元程序紧密结合。

Metaprograms 并不是特别神秘或复杂

写一个元程序(metaprogram),其实就是写一个**constexpr代码块**,例如:

cpp 复制代码
constexpr {
  // 做一些编译期的操作
}

这个写法在语义上等价于:

cpp 复制代码
constexpr void metaprogram() {
  // 做一些编译期的操作
}
constexpr int dummy = (metaprogram(), 0);

换句话说,元程序就是在编译期间执行的constexpr函数调用 ,并没有什么特别的语法本质,只是把代码写在编译时执行的上下文里。

你可以理解成:元程序其实就是一种利用constexpr函数做的编译期计算和代码生成机制,语法上可以更方便,但本质就是constexpr函数执行而已。

元程序(metaprograms)如何用于"注入"代码 ,用一个 to_string 函数的例子来说明:

代码示例简析:

cpp 复制代码
template<Enum E>
const char* to_string(E value) {
  switch (value) constexpr {
    for... (auto e : $E.enumerators()) -> {
      case e.value(): return e.name();
    }
  }
}

这段代码"意味着"什么?

  • $E.enumerators() 是一个反射操作,得到枚举类型 E 的所有枚举成员的集合。
  • for... (auto e : $E.enumerators()) -> { ... } 是一个元程序的循环展开,表示"对枚举的每个成员 e,注入(inject)一个 case 语句"。
  • 也就是说,编译器会把这段代码自动展开成:
cpp 复制代码
switch(value) {
  case EnumValue1: return "EnumValue1";
  case EnumValue2: return "EnumValue2";
  case EnumValue3: return "EnumValue3";
  // ... 枚举中所有值对应的 case 语句
}
  • 这里的"注入(injection)"指的是switch 语句里动态生成多个 case 分支代码,而不是手写的。

结合Herb Sutter的讲座

  • 元程序就是编译时执行的程序,可以读取程序结构(比如枚举成员),并生成对应的源代码(比如case语句)
  • 这样既避免了手写大量重复代码,也能保证代码和枚举类型总是同步一致。
  • 这是利用编译期反射和代码生成的一个典型例子。

总结

  • 元程序利用反射拿到结构信息(如枚举成员)
  • 在编译期动态"注入"代码片段(如case语句)
  • 生成最终的函数代码,提高安全性和简洁度

内容主要讲的是**代码注入(Code Injection)**的本质和实现思路:

代码注入到底是什么?

  • 代码注入就是把"生成的代码"放进程序的某个特定位置。
  • 那么"注入"的到底是什么呢?
    • 是简单的**词法单元(tokens)**吗?
    • 是一段字符串(source code string)吗?
    • 还是一段"代码片段(fragments of source code)"?

源代码作为数据(Source code as data)

  • 注入的对象不是简单字符串,而是反射得到的"源代码片段",也就是程序的结构化表示。
  • 我们想注入的可以是:
    • 新声明(class、function、variable等)
    • 新语句(在函数体内插入语句)
    • 目前还不支持表达式的注入(不完全支持,但未来可能会有)。
  • 注入操作符接收的输入是反射对象(reflection),可以理解为"对代码的抽象描述"。

源代码字面量(Source code literals)

  • 类似于 auto frag = __fragment class C { ... },这里的 __fragment 是一个关键字(伪代码,表示源码片段)。
  • frag 是一个反射对象,表示类 C 的"片段",里面包含成员变量和成员函数。
  • 这个类片段能被注入到别的地方,比如把它插入到一个已有的类定义中。

不同类型的代码片段

  • 类片段(class fragments):包含成员声明,能插入类内部。
  • 命名空间片段(namespace fragments):包含命名空间内的声明,能插入命名空间。
  • 代码块片段(block fragments):包含语句,能插入函数体内部。

总结

  • 代码注入不是简单地拼字符串,而是操作结构化的代码片段。
  • 反射机制提供对代码结构的访问,元程序则能生成这些结构化代码片段。
  • 注入操作符把这些结构化代码片段放到程序的合适位置,实现了编译期的"代码生成+代码插入"。

你这段内容主要讲的是代码片段(fragments)的"注入上下文"限制,以及"类片段"的特点,理解起来可以总结如下:

注入上下文(Injection Context)

  • 代码片段只能注入到匹配的上下文中:
    • 类片段(class fragments)只能注入到类(class)中
      例如,把一个成员变量和成员函数的代码片段插入已有类里。
    • 命名空间片段(namespace fragments)只能注入到命名空间中
      例如,插入新的函数、类型定义等。
    • 代码块片段(block fragments)只能注入到函数体中(statement lists)
  • 有可能将命名空间片段注入到类中,
    但要求插入的成员是 static(静态成员),
    这可以看作是"拓展类的静态成员",虽然还没实现。

类片段示例

cpp 复制代码
auto frag = __fragment class Node {
  Node* next;
  Node* prev;
};
  • 这个片段代表一个类成员集合,包含两个指针成员。
  • 类片段可以有名字,支持自引用(比如这里的 Node* 自己指向同类型)。
  • 这样,元程序就能把这段代码注入到另一个类里,扩展该类的成员。

你的理解总结

  • 代码注入必须符合上下文的类型限制,避免语法和语义错误。
  • 类片段允许自引用,方便复杂类型的构造。

这段内容讲的是命名空间片段(namespace fragments),理解可以总结为:

命名空间片段(Namespace Fragments)

  • 类似于类片段 ,但作用域是命名空间
    用来定义或注入命名空间范围的成员,比如函数、类型别名、变量等。
  • 代码示例:
cpp 复制代码
auto ns = __fragment namespace N {  // 注意:这里 N 不是可选的(是一个 BUG)
  operator==(const MyType& a, const MyType& b) {
    return a.equal(b);
  }
};
  • 这个片段定义了命名空间 N 下的一个 operator==
    方便通过元编程注入该比较操作符。
  • BUG提示
    这里提到的 BUG 是说,在语法上,命名空间片段必须指定名字(这里是 N
    不能像类片段那样"名字可选"。
    可能后续会修正,允许匿名命名空间片段或者支持不同用法。

你的理解总结

  • 命名空间片段和类片段类似,都是用来描述可以注入的代码块,但作用域不同。
  • 目前命名空间片段要求显式指定命名空间名字。
  • 主要用于元编程时批量注入函数、类型、变量等到命名空间。

这段内容讲的是块片段(block fragments),理解如下:

块片段(Block Fragments)

  • 本质上就是一个复合语句 (compound statement),也就是一段代码块 { ... }
  • 代码示例:
cpp 复制代码
auto frag = __fragment {
  case 0:
    return 42;
};
  • 这个片段定义了一个包含 case 0: 标签的代码块,
    可能用于注入到 switch 语句中,动态生成分支代码。
  • 作用是:
    方便元编程时向函数体、语句块里注入新的语句或控制流分支。

总结

  • 块片段 = 代码块(语句序列),用于注入到函数体或控制流结构中。
  • 可用来注入 case 标签、if 语句、循环等语句片段。
  • 是可注入的"代码碎片"之一,配合反射与元编程,实现灵活的代码生成。

理解这段内容的关键是"参数化的源码字面量":

Parameterized literals(参数化字面量)概念:

  • 通常我们在元编程里生成的代码片段(fragment)想依赖当前上下文中的局部变量,而不是写死的常量。
  • 通过使用元语言的能力,可以在源码片段里动态"捕获"并使用当前作用域的变量。

示例说明:

cpp 复制代码
int num = 0;
for... (auto x : $.member_variables()) {
  auto frag = __fragment class {
    int idexpr("var_", num); 
  };
  ++num;
}
  • 这里的 num 是循环外的局部变量。
  • 每次循环时,生成的 class 片段中会根据当前的 num 生成一个成员变量名,比如 var_0, var_1, var_2 等。
  • 这种写法表示源码片段里的代码会动态包含 num 当前的值。

本质:

  • 参数化的字面量可以"捕获"作用域变量,在生成的代码中体现这些变量的值。
  • 使元编程更灵活,可以写出根据上下文不同而变化的代码生成片段。

理解这部分内容的关键点在于源码字面量(source code literals)编译和使用的三个阶段,以及它们和模板实例化的类比:

1. 三个阶段

  • 解析阶段(Parse the fragment)
    源码片段先被编译器解析:进行名称查找(name lookup)、类型检查等语法语义分析。
    此时源码片段中的局部变量名被标记为依赖占位符(dependent placeholders),即占位等待后续绑定。
  • 确定源码字面量的类型(Determine the type)
    编译器判断该源码字面量代表的是类片段、命名空间片段还是代码块片段。
  • 计算源码字面量的值(Compute value)
    这一步会在实际注入时,将之前标记的占位符替换成对应的捕获变量的实际值。

2. 源码片段如模板

源码片段非常像模板:

  • 占位符变量类似模板参数
  • 但不使用模板语法,而是用元编程系统隐式捕获并替换
  • 解析时"记住"哪些名称是占位符,实际使用时替换为具体的值

3. 示例说明

cpp 复制代码
class __fragment__ {
  auto n = <magic>; // 这里的 <magic> 是占位符,指向外部的变量 n
  auto x = <magic>; // 同理,x 也是占位符
};
class <unnamed> {
  int idexpr("var_", n); // 由于 n 是占位符,此时还不能形成最终标识符名
};

这表明源码片段的真正含义要等到注入时才能完整展开。

总结

  • 源码字面量需要先解析带占位符的代码模板,再用捕获的值替换占位符,最后生成完整代码。
  • 整个过程与模板实例化很相似,但更直接地以代码片段和变量捕获的形式出现。

理解这段内容的关键点是源码字面量的类型如何定义,它体现了源码字面量作为一种"封装了反射信息和捕获变量的独特类"的概念。

核心要点:

  1. 源码字面量的类型是一个唯一的类(unique class)
    这个类不仅仅代表源码片段本身,还携带了反射信息 ,比如它对应哪个类、函数或者其他构造的反射句柄(meta::class_type<X>);
  2. 该类包含捕获的局部变量
    源码字面量可能捕获了外部的局部变量(比如前面提到的 xn),这些变量以类的成员变量形式保存;
  3. 构造函数用于初始化捕获变量
    该类的构造函数接受捕获变量作为参数,并初始化对应成员;

以代码片段解释:

cpp 复制代码
struct __fragment_k : meta::class_type<X> {
    const decltype(x) captured_x; // 捕获的局部变量 x
    const int captured_n;         // 捕获的局部变量 n
    constexpr __fragment_k(decltype(x) x, int n)
        : captured_x(x), captured_n(n)
    { }
};
  • __fragment_k 是源码字面量的类型,继承自 meta::class_type<X>,表示反射信息。
  • captured_xcaptured_n 是源码片段中捕获的局部变量,存储在类型实例里。
  • 构造函数接收这些局部变量,方便后续注入源码时用它们替换占位符。

直观理解:

  • 这个源码字面量就像一个封装了"元信息 + 捕获上下文变量"的数据包
  • 当我们把这个"包"注入代码时,就能展开成具体的代码片段,且能引用这些捕获的变量。

这里强调的是:

cpp 复制代码
struct __fragment_k : meta::class_type<X> {
  • 源码字面量的类型(__fragment_k)是从一个反映该源码片段的"类类型"继承而来
  • 其中 X 是一个编译器内部对该源码片段的唯一标识句柄(handle),也就是反射的核心信息,
  • 这个类型的名字是唯一的,意味着每个源码字面量片段都会有它自己对应的、唯一的类类型,便于编译器区分不同的代码片段。
    简单说,就是每段源码字面量都对应一个带有反射信息的独特类型,这个类型封装了这段代码的元数据和捕获的上下文,方便后续操作和注入。

源码字面量的类型设计中,成员变量用来保存捕获的局部变量,并且构造函数用来初始化它们。

我帮你把这段内容完整、规范地写出来:

cpp 复制代码
// 假设这是表示源码字面量片段的反射基类
struct meta_class_type {
  // 这里可能存放片段的元信息
};
// 源码字面量对应的具体类型,保存捕获的局部变量
struct __fragment_k : meta_class_type {
  const decltype(x) captured_x;  // 捕获变量 x 的类型和值
  const int captured_n;          // 捕获变量 n
  // 构造函数,用来初始化捕获的变量
  constexpr __fragment_k(decltype(x) x, int n)
    : captured_x(x), captured_n(n)
  { }
};
  • captured_x 使用了 decltype(x) 以确保类型与捕获的变量一致
  • captured_n 直接用 int,因为它就是个整数
  • 构造函数初始化列表保证成员常量的正确初始化
    这就是源码字面量类型的典型设计,方便在注入(injection)阶段将捕获的变量值带入片段。

这段说的是源码字面量(source code literal)的 ,就是用对应的类型(比如你之前提到的 __fragment_k)的构造函数,传入捕获的变量值,来构造一个对象。

举个代码例子:

cpp 复制代码
// 假设之前定义了源码字面量类型 __fragment_k
constexpr __fragment_k frag(x, n);

这里的 frag 是一个对象,

  • 它的类型 __fragment_k 内含对源码片段的反射信息
  • 它保存了所有捕获的变量(如 xn)的当前值
    这个值 frag 就携带了完整信息,能被用来注入 新的代码(比如生成一个新类、成员等),实现代码生成。
    简单总结:
    源码字面量的"值"就是该字面量类型的实例,实例里封装了被捕获的局部变量的值,这些信息之后可以被用来动态生成或修改代码。

这里讲的是**代码注入(injection)**的几种方式,区别主要在于注入点的不同:

  • __generate ------ 代码会被注入到某个地方(位置可能由系统决定或者推断),不一定是当前代码点。
  • __inject ------ 代码直接注入到当前所在的位置,也就是说写这句代码的地方就会插入新的代码。
  • __extend ------ 代码注入到别处 (通常是一个特定类、命名空间、函数外部等),从当前代码点跳转到别处注入。
    无论哪种注入方式,操作本质相同,都是把"源码字面量"或"反射得到的代码片段"插入到目标上下文。
    上下文指的是你注入代码的环境,比如:
  • 命名空间
  • 函数体
    不同上下文会影响注入代码的语法和效果,比如:
  • 在类里注入成员变量或成员函数
  • 在函数体里注入语句
  • 在命名空间里注入自由函数或类型别名等
    这三种注入机制是为了给元编程和代码生成提供灵活性,方便开发者控制代码插入的位置和时机。

这里说的是**注入操作符(injection operators)**的两种用法:

  1. 注入反射实体(reflection)
    直接注入一个通过反射得到的实体(比如一个类、函数、成员变量等)的代码表示。
    例:

    cpp 复制代码
    __inject $MyClass;  // 把通过反射获得的 MyClass 注入到当前上下文
  2. 注入源码片段(fragment)
    注入一段源码片段。源码片段本质上是一个__fragment对象的简写形式。
    例:

    cpp 复制代码
    __inject {
        void foo() { /*...*/ }
    };

    等价于:

    cpp 复制代码
    auto f = __fragment {
        void foo() { /*...*/ }
    };
    __inject f;

总结:

注入操作符支持两种输入:

  • 反射获得的"完整实体"直接注入
  • 源码片段构造的代码块注入
    这样可以方便元编程时,既能注入已有的代码反射结果,也能动态生成代码片段再注入。

这里讲的是**代码生成(generation)**的概念,特别是在元编程(metaprogramming)中的应用:

  • 你写一个constexpr块,里面调用几个"生成"函数(例如make_equality_comparable<MyType>()make_totally_ordered<MyType>()),这些函数是元程序,它们会"替换"这个constexpr块为一系列注入的代码片段,比如生成operator==operator<等函数定义。
  • 这允许你把复杂的元程序拆分成更小、更模块化的函数,每个函数负责生成一部分代码,然后注入到目标类或命名空间中。
  • 代码最终不是简单的函数调用 ,而是通过注入操作符将生成的代码片段插入到相应位置,完成代码自动扩展。
    简而言之,generation 是用来把元编程写的代码"展开"为实际的、可执行的C++代码定义,非常适合自动生成重载、比较操作符、序列化函数等等。

这段话讲的是 generation(代码生成) 的具体实现细节:

  • __generate 操作符不会马上把代码插入当前点,而是排队等待注入,通常是在合适的"后续阶段"才执行注入(比如类定义结束后,或者命名空间作用域中)。
  • 举例中的 make_equality_comparable 模板函数里,用 __generate namespace { ... } 来生成一个全局的 operator==,比较两个 T 类型的对象。
  • 这里的"注入点"由上下文决定,而不是由 __generate 明确指明。这意味着代码会被注入到"合适的地方",例如对应的命名空间中。
  • 这样设计允许元程序聚合多个注入请求,然后统一管理,避免注入时序混乱。
    简单示例:
cpp 复制代码
template <typename T>
constexpr void make_equality_comparable() {
  __generate namespace {
    bool operator==(T a, T b) { return a.equal(b); }
  };
}

调用后,operator== 会在 T 所在的命名空间被自动生成。

这就是 代码生成的延迟注入(queued injection) 的思想,方便元编程组织和维护自动生成的代码。

这里是 generation(代码生成) 的"前后对比"示意:

cpp 复制代码
constexpr {
  make_equality_comparable<MyType>();
  make_totally_ordered<MyType>();
}

调用两个元程序,期望自动生成比较运算符。

cpp 复制代码
bool operator==(MyType a, MyType b) { return a.equal(b); }

经过元程序执行和代码注入后,生成了具体的 operator== 函数实现。

也就是说:

  • 元程序写出"高层抽象"的自动生成逻辑。
  • 编译器或反射系统通过 __generate 等操作符,把具体代码注入到合适位置。
  • 最终,源码中就有了自动生成的函数,供程序调用。
    这就是"程序在编译时生成代码",简化程序员的重复劳动,同时保证代码安全和一致性。

这段讲的是 代码注入(injection) ,特别是"__inject"操作符的用法:

cpp 复制代码
struct MyList {
  __inject make_links();
};
  • __inject make_links(); 表示把 make_links() 生成的代码片段直接注入当前类MyList)中。
  • 例如,make_links() 可能会生成 nextprev 指针成员。
  • 这样就能把复杂类拆解成多个可配置、可复用的组件,每个组件用一个元程序生成并注入。
    关键点:
  • 注入位置是"这里",也就是元程序写在哪,代码就生成在哪。
  • 这种方式方便构造复杂类型,代码结构清晰。

这段内容详细展示了用元程序构造类成员并注入的机制:

cpp 复制代码
template <SmallType T>
constexpr auto make_links() {
  return __fragment class C {
    C* next;
    C* prev;
  };
}
  • make_links() 是一个返回类片段(class fragment)的函数。
  • 这个类片段包含了 nextprev 两个指针成员,指向同类型的 C
cpp 复制代码
struct MyList {
  __inject make_links();
};
  • 使用 __inject 调用 make_links(),意味着将返回的类片段成员注入 MyList
  • 经过注入后,MyList 变成:
cpp 复制代码
struct MyList {
  MyList* next;
  MyList* prev;
};

关键点:

  • __fragment class C { ... } 里的 C 是"占位符"类型,注入时会被注入到当前类的名字(这里是 MyList)替代。
  • 这样写,能够动态地"生成"和"注入"类成员,非常强大灵活。
    总结:
  • 你写一个生成类成员的元程序(这里是 make_links())。
  • 在类中用 __inject 调用它,自动把成员注入当前类。
  • 实现了模块化、可复用的代码生成。

这段话说明了 __inject__generate 的等价性:

cpp 复制代码
struct MyList {
  __inject make_links();
};

等价于:

cpp 复制代码
struct MyList {
  constexpr {
    __generate make_links();
  }
};

区别和联系:

  • __inject make_links(); 表示"立即在当前类的定义处注入 make_links() 生成的成员"。
  • __generate make_links(); 表示"将 make_links() 生成的代码排队,稍后注入",而用 constexpr { ... } 包裹表示这是一个元程序块。
  • 在类定义内,__inject 实际上相当于"立刻注入",__generate 是"排队注入",但放在 constexpr 块内也就是元程序执行上下文,效果相同。
    总结:
  • 语法不同,语义相同。
  • __inject 更简洁,__generate 更明确地表现出元程序运行和代码生成过程。

这段话讲的是"身份注入"(identity injection)的概念:

cpp 复制代码
struct MyList {
  __inject class { int i; }
};

这其实和直接写:

cpp 复制代码
struct MyList {
  int i;
};

完全等价

也就是说,__inject 直接注入一个匿名类片段时,实际效果就是把那个片段展开成普通成员,插入到当前类中。

简而言之,__inject 不做任何包装或改动,只是将片段里的成员直接放进注入点所在的类里。

__extend 用于"延展"一个已经存在的类或命名空间,往里面注入新的成员,比如方法、成员变量等。

示例:

cpp 复制代码
__extend ($MyList) class {
  void sort() { 
    // ... 排序实现 
  }
};

这段代码的意思是:把 sort() 函数注入到已有的 MyList 类的作用域里。

__inject 不同的是,__inject在当前上下文 直接注入代码,而 __extend 是明确指定往别处注入 (这里是往 $MyList 这个类注入)。

用法场景:

  • 逐步构建类,比如先定义基础成员,再在不同地方延展出各种功能函数
  • 在不修改原始类定义的情况下,后期往类里添加额外代码(类似"开闭原则"的一种实现)
    总结:
  • __inject ------ "现在这里注入"
  • __extend ------ "注入到指定目标那里"

这段内容的关键点是:

  • __extend 用来 修改已经闭合的类定义 ,给类添加新的成员函数(如 sort())。
  • 对于命名空间,__extend 类似于普通的"打开命名空间"操作,没有太大新意。
  • 但和 __generate__inject 不同,__extend 不能"添加任何会改变类布局的东西",比如成员变量 ,因为这会影响类的大小和内存布局,容易引发问题。
    举个总结:
cpp 复制代码
// 原始类
struct MyList { };
// 用 __extend 给 MyList 添加函数
__extend ($MyList) class {
  void sort() { /* 排序实现 */ }
};
// 实际效果
struct MyList {
  void sort();
};

但你不能这么写:

cpp 复制代码
__extend ($MyList) class {
  int new_member; // 错误!不能改变类布局
};

因为这会破坏类的内存结构。

所以,__extend 主要用来后期往类里加行为(函数),而不是加状态(数据成员)。

* 可以用注入操作符(__inject)直接把一个已存在的声明注入到另一个作用域

  • 例如:
cpp 复制代码
struct S { int a, b, c; };
struct T {
  __inject S;  // 直接把 S 的整个声明注入到 T 里
};
  • 这样,T 里面就包含了 S 的成员(a, b, c)。
  • 也可以用 constexpr 和循环结合,遍历 S 的成员变量,逐一生成注入:
cpp 复制代码
struct T {
  constexpr {
    for... (auto x : $S.member_variables())
      __generate $x;  // 逐个生成 S 的成员变量注入 T
  }
};
  • 重要提醒:
    • 如果注入的不是代码片段(fragment),而是完整声明,则会注入整个声明,而不仅仅是成员
      总结:
  • __inject S; 是直接"复制"整个 S 这个声明进 T
  • 使用循环+__generate 可以更细粒度地控制注入成员。

这段讲的是:

  • 可以在注入(__generate)之前,先修改反射得到的声明(reflection)
  • 例子:
cpp 复制代码
struct S {
  constexpr {
    auto x = $S::fn;   // 反射得到函数 fn
    x.make_virtual();  // 修改这个反射对象,使函数变为 virtual
    __generate x;      // 注入修改后的函数声明
  }
};
  • 这里的修改只影响注射的那个"反射对象"副本,不会改变原始的声明。
  • 也就是说,原来的函数 fn 仍然保持不变,但注入时会变成 virtual
    总结:
  • 反射对象是可修改的"镜像",可以用来生成带修改的声明。
  • 不破坏原始代码,只影响生成的代码。

这部分讲了注入(Injection)的工作原理和细节:

Injection 是如何工作的?

  • 类似于模板实例化的过程。
  • 关键在于处理"对片段名字"的引用。
  • 需要将占位符(placeholders)替换成实际的值
  • 还有一些额外的细节,确保名字查找(lookup)能够找到正确的实体。

注入的细节

  • 注入本质上是一个反射(reflection)对象,代表要注入的代码片段(fragment)。
  • 注入类型编码了对原始片段的引用,值则保存了占位符的替换值。
  • 注入目标(injectee) 是片段将要注入的上下文(类、命名空间或函数)。
  • 注入过程会实例化片段中的成员(类成员、命名空间成员或语句):
    • 用注入目标替换片段中对片段自身的引用
      换句话说,注入时会把片段代码中的"模板变量"或者"占位符"替换成实际的上下文,确保生成的代码能正确地绑定到当前环境。

这是一个注入(Injection)过程的具体示例 walkthrough,步骤如下:

代码示例:

cpp 复制代码
struct S { int n; bool b; };
struct Blah {
  constexpr {
    int n = 0;
    for... (auto x : $S.member_variables()) {
      __generate class {
        typename($x.type()) idexpr("var_", n);
      };
      ++n;
    }
  }
};

逐步解释:

  1. 结构体 S 有两个成员变量: int n; bool b;
  2. 在 Blah 的 constexpr 块里:
    • 定义一个计数器 n = 0;
    • 通过反射遍历 $S.member_variables(),即访问 S 的成员变量列表(分别是 nb)。
  3. 对每个成员变量 x:
    • 生成(__generate)一个匿名类片段 ,包含一个成员:

      cpp 复制代码
      typename($x.type()) idexpr("var_", n);
    • 这里:

      • typename($x.type()) 是成员变量的类型,比如 intbool
      • idexpr("var_", n) 生成一个名字,例如 "var_0", "var_1" 等。
  4. 计数器 n 自增,为下一个成员变量生成不同的名字。

结果预期:

这段代码相当于生成了:

cpp 复制代码
struct Blah {
  class {
    int var_0;
  };
  class {
    bool var_1;
  };
};

(匿名的内嵌类成员)

总结:

  • 这个示例演示了如何用反射遍历已有类型的成员变量,并基于它们的类型和计数器生成新的类成员代码。
  • __generate 让生成的代码注入到某个上下文 (这里是 Blah)。
  • 利用 idexpr 生成动态的标识符名字。

这段代码是把反射的循环展开成两个独立的代码块,每个块处理 S 结构体的一个成员变量,并用 __generate 注入相应代码。

具体解析:

cpp 复制代码
{
  auto x = get<0>($S.member_variables()); // 反射到第一个成员 int S::n
  __generate class {
    typename($x.type()) idexpr("var_", n);
  };
}
{
  auto x = get<1>($S.member_variables()); // 反射到第二个成员 bool S::b
  __generate class {
    typename($x.type()) idexpr("var_", n);
  };
}
  • 每个 {} 代码块里:
    • auto x = get<i>($S.member_variables()); 获取成员变量的反射对象。
    • typename($x.type()) 生成对应成员变量的类型。
    • idexpr("var_", n) 生成成员变量名字(例如 var_0, var_1)。
    • __generate class { ... } 注入一个匿名类,类中有一个成员变量。

总结

这就是反射循环的"编译时展开",把循环变成多个代码块,每个块完成一个成员变量的代码生成。等价于你在写:

cpp 复制代码
class {
  int var_0;
};
class {
  bool var_1;
};

不过这里用的是模板和反射机制自动生成。

梳理下这段代码的含义和工作机制。

Injection: Typing 详解

cpp 复制代码
{
  auto x = get<0>($S.member_variables()); // 取 S 的第一个成员变量,类型是 int S::n
  __generate class {
    typename($x.type()) idexpr("var_", n);
  };
}

这里,__generate 表示要生成代码片段,这个代码片段是一个匿名类,里面有一个成员变量:

  • 类型是 typename($x.type()),即成员变量 x 的类型;
  • 名称是 idexpr("var_", n),表示拼接字符串 "var_" 和局部变量 n 生成变量名。

这个代码片段对应的类型结构:

cpp 复制代码
struct __fragment_1 : meta::class_type<X1> {
  meta::field<X2> captured_x;  // 捕获的成员变量反射对象
  int captured_n;              // 捕获的局部变量 n
};
  • __fragment_1 是这个代码片段的唯一类型,用来存储代码片段和捕获的值。
  • 它派生自 meta::class_type<X1>X1 是编译器对该代码片段的反射表示。
  • captured_x 是一个字段,表示捕获的成员变量反射对象(x),用来在注入时访问变量的类型、名字等信息。
  • captured_n 是捕获的局部整数变量,用于生成变量名。

工作流程

  1. 获取反射数据 :用 $S.member_variables() 得到成员变量列表,通过 get<0> 取第一个成员。
  2. 构造代码片段:生成一个匿名类片段,成员变量类型和名称基于反射和局部变量。
  3. 生成类型 :这个代码片段的类型 __fragment_1 包含所有捕获的上下文信息。
  4. 注入时替换 :编译器用 captured_xcaptured_n 替换代码片段中对应的占位符,最终生成具体成员变量声明。

这段讲的是注入(injection)过程中的"替换"(substitution)机制,主要有三种替换:

Injection substitution 三种替换

  1. Enclosing context(包围上下文)
    • 代码片段中原本指向包围它的上下文(比如它定义时的类或命名空间)的引用
    • 在注入时,这些引用被替换为目标"注入点"的上下文
    • 比如:代码片段里写的是this->member,注入到新的类时,this就指新类
  2. Placeholders(占位符)
    • 代码片段里出现的"占位符"名字(通常是局部变量名或特定标识)
    • 被替换成注入时捕获的对应常量表达式
    • 这就是"捕获值"的用处,保证代码片段里的占位符用实际值替代
  3. Non-local names(非局部名字)
    • 代码片段中对外部上下文成员的引用
    • 在注入后改成在"注入点"去查找(lookup)
    • 确保成员名字从注入点上下文解析,而非原上下文,避免引用错位

总结

注入时,代码的所有上下文引用都会被智能"重定向"到正确的目标,从而保证生成的代码在新位置语义正确,且捕获的值都能正确展开。

这段描述了注入(injection)过程中的**求值(evaluation)**步骤,具体如下:

注入求值过程示例

cpp 复制代码
{
  // 取得第一个成员变量:int S::n
  auto x = get<0>($S.member_variables());
  __generate class {
    typename($x.type()) idexpr("var_", n);
  };
}
求值步骤
  1. 原始表达式

    cpp 复制代码
    typename($x.type()) idexpr("var_", n);

    这里:

    • $x.type() 是反射表达式,表示成员变量的类型
    • idexpr("var_", n) 是动态构造变量名,结合字符串和局部变量 n
  2. 替换后(substitution)
    $x.type() 替换成其具体类型反射描述 meta::field<X2>{}.type()n 替换成捕获值 0

    cpp 复制代码
    typename(meta::field<X2>{}.type()) idexpr("var_", 0);
  3. 应用投影(projections)
    meta::field<X2>{}.type() 投影出实际类型 int
    idexpr("var_", 0) 生成变量名 var_0
    最终展开成:

    cpp 复制代码
    int var_0;

总结

  • 反射值经过替换成具体的类型和值
  • 投影操作将反射值转化为真实的类型和标识符
  • 这样注入的代码片段被具体化成有效的C++代码

这是这整个注入过程的最终结果:

原始定义:

cpp 复制代码
struct S { int n; bool b; };
struct Blah {
  constexpr {
    int n = 0;
    for... (auto x : $S.member_variables()) {
      __generate class {
        typename($x.type()) idexpr("var_", n);
      };
      ++n;
    }
  };
};

经过注入展开和求值,最终 Blah 结构体等价于:

cpp 复制代码
struct Blah {
  int var_0;    // 对应 S::n 的类型和成员名
  bool var_1;   // 对应 S::b 的类型和成员名
};

这说明:

  • 通过反射 $S.member_variables() 得到成员列表;
  • 遍历每个成员,动态生成新的成员变量(带编号的变量名);
  • 生成的成员变量类型和原始成员类型一致。
    这种机制支持基于已有类的结构自动生成类成员,极大提升代码复用和元编程能力。
    如果你想,我可以帮你总结这个过程的核心代码或实现思路!

注入机制(injection operators)来构建更高级的元类(metaclasses)

总结一下:

从注入到元类的关键点

  • 元类其实是基于注入的封装
    你可以把元类看成一个模板函数(interface<T>),它利用注入操作,生成一个以 T 命名的类定义。
  • 类关键字的重定义
    using interface -> struct;
    这条语句意味着把 interface 绑定为一个新的类关键字,实际上就是给它起个别名,方便语法上使用。
  • 命名空间封装
    interface IShape { ... } 并不是直接定义 IShape,而是定义一个命名空间里的 IShape_proto::IShape 结构体,然后通过注入把它映射到最终作用域。

伪代码示意

cpp 复制代码
template<typename T>
constexpr void interface() {
  // 利用注入生成 T 这个类
}
// 绑定新的语法
using interface -> struct;
// 语法糖示意
interface IShape {
  // ...
};
// 等价于
namespace IShape_proto {
  struct IShape {
    // ...
  };
}
constexpr {
  interface<IShape_proto::IShape>();
}

理解了,总结一下这最后的部分:

结论

  • 元编程需要自底向上的方法,不是一步到位的"大跃进"。
  • 步骤顺序:
    1. 反射(Reflection):先能读取已有代码结构和信息。
    2. 源代码字面量(Source code literals):将代码片段作为数据表达。
    3. 原始注入器(Primitive injectors):基础的代码插入工具。
    4. 元类(Metaclasses):高级抽象,利用前面所有工具生成和管理代码结构。
  • 还没涵盖作者过去一个月实现的所有内容,说明这是一个不断发展的系统。
相关推荐
翱翔的小菜鸟11 分钟前
Java Stream API中peek()方法使用不当引发的生产问题
java·开发语言
愚润求学27 分钟前
【递归,搜索与回溯算法】记忆化搜索(二)
linux·c++·算法·leetcode
来两个炸鸡腿37 分钟前
【Datawhale组队学习202506】YOLO-Master task03 IOU总结
python·学习·yolo
狐凄1 小时前
Python实例题:基于联邦学习的隐私保护 AI 系统(分布式学习、隐私计算)
开发语言·python
NLxxxxX1 小时前
爬虫获取数据:selenium的应用
开发语言·爬虫·python·selenium·测试工具·numpy·pandas
凌佚1 小时前
rknn优化教程(三)
c++·yolo·目标检测
专注VB编程开发20年1 小时前
C# .NET多线程异步记录日声,队列LOG
java·开发语言·前端·数据库·c#
charlie1145141911 小时前
从C++编程入手设计模式——责任链模式
c++·设计模式·责任链模式
专注VB编程开发20年1 小时前
c#,vb.net LockObject ,多线程锁,多线程安全字典ConcurrentDictionary
开发语言·c#·.net
慧慧吖@2 小时前
箭头函数的this指向
开发语言·前端·javascript