编程语言的元编程模型

什么是元编程

wiki百科定义如下:

1
Metaprogramming is a programming technique in which computer programs have the ability to treat other programs as their data. It means that a program can be designed to read, generate, analyze or transform other programs, and even modify itself while running.[1][2] In some cases, this allows programmers to minimize the number of lines of code to express a solution, in turn reducing development time.[3] It also allows programs greater flexibility to efficiently handle new situations without recompilation.

wiki的解释简单总结就是——元编程主要指程序可以把程序本身当做数据来操作从而避免编码时的重复工作以及增加编程语言的表达能力。现代编程语言支持元编程可以简单总结为:用代码生成代码。

支持元编程的几种方式

  • 泛型
  • 反射
  • 模板

泛型

使用泛型类型可以避免写功能类似的冗余代码,提升编码效率。不同编程语言的泛型实现方式不同,一般是两种策略:

  • 装箱:装箱是指我们把所有的东西都放在统一的 "盒子 "里,使它们的行为方式都一样。通常是通过在堆上分配内存,只在数据结构中放指针来实现的。我们可以让不同类型的指针有同样的行为方式,这样,同样的代码就可以处理所有的数据类型了。然而这种做法可能要付出额外的内存分配、动态查找和缓存丢失的代价。在C语言中,这相当于让你的数据结构存储void*指针。
  • 单态化:单态化是针对我们要处理的不同类型的数据,多次复制代码。这样每份代码都直接使用对应的数据结构和函数,而不需要任何动态查找。这样运行效率足够快,但代价是代码大小和编译时间的膨胀,因为同样的代码只要稍加调整就会被编译多次。

基于装箱实现的泛型

  • java: 编译前进行类型擦除,处理成Object类型,编译器不感知泛型。运行时结合具体上下文可以确定类型。
  • golang: interface{}
  • C: void*
  • rust动态分派: rust返回泛型对象时,依靠trait object来实现,参考Box。在运行期间确定具体指向的类型。

基于装箱实现泛型需要考虑运行时需要暴露类型特化函数的需求。现在一般有以下两种实现方式,主要是第一种比较主流:

  • vtables(虚拟方法表,主流的方式): 主要思路就是让对象关联vtables,其大致上的结构如下,即有个指针指向虚拟方法表,这样可以让通用代码为每个类型查找特定类型的函数指针。拓展下,如果对象可以关联vtable,自然也可以关联其他类型信息,比如字段名字、类型、方法名等,反射就可以这样实现。

image.png

  • 字典传递:像OCaml将需要的函数指针表作为一个隐藏的参数传递

基于单态化实现的泛型

例如rust的静态分派,函数通过trait bounds声明好具体的泛型,编译器在编译期间确定好具体的类型,并且生成多份代码

宏也是实现元编程的关键特性。一般分为如下两种:

  • 基于文本替换的宏:主要是C和C++,直接做文本替换,比较简单
  • 基于AST的宏
    • 声明宏:声明宏,就是完全基于词条流(TokenStream)。声明宏的展开过程,其实就是根据指定的匹配规则(类似于正则表达式),将匹配的 Token 替换为指定的 Token 从而达到代码生成的目的。因为仅仅是 Token 的替换(这种替换依然比 C 语言里的那种宏强大),所以你无法在这个过程中进行各种类型计算。
    • 过程宏:过程宏也是基于 TokenSteam API的,只不过由第三方库作者 dtolnay 设计了一套语言外的 AST ,经过这一层 AST 的操作,就达到了类似操作AST的效果。rust就采用了过程宏,之所以不直接暴露AST API是因为AST和语法特性紧相关,经常调整。像rust这种过程宏应该是当下比较理想的实现方式了

反射

有反射,则程序可以在运行时根据反射的信息动态生成新的程序,比如新增字段或者方法。此外反射还可以方便程序对泛型类型进行序列化和反序列化。像go、java都在编程语言级别内建了这种机制。但是像rust这种语言没有原生的支持反射,毕竟反射显然对于性能是个巨大的损耗,同时运行时确定的行为也容易引发更多稳定性风险。

参考资料