元编程,模板与反射浅谈: 以获取变量名称为例


何为"元(meta)"

元(meta)的一种含义是"关于自身的" (Meta is a prefix meaning "referring to itself")
我们可以理解为 元-X = X的Xmeta-sth. = sth. of sth.

例如:

  • "哲学是科学之科学"(哲学是元科学)
  • 元宇宙(一种根据个人特点生成不同的"宇宙"的技术)

元编程 Metaprogramming

元编程可以理解为设计程序的程序,比较常见的用法有模板和反射
传统的程序编译过程可以简化为: 代码->指令,一经编译,代码就不可再变,也不可再知,但在实践中,我们常常需要根据不同的情况改变或了解代码,比如:我们需要在运行中输出变量的名字

例子:如何打印一个变量的名字

想一下,如何在C语言中打印一个变量的名字?例如我们想实现一个函数用来调试错误:

void dbg(int x) // 接收一个变量x,输出x的名字和对应的值
{
	// 在这里编写代码
	// 不允许使用预处理,它也能算一种原始的元编程方式
}
int main()
{
	int n = 5;
	dbg(n); // 输出 "n = 5"
}

这是不可能的!
我们编写代码的地方只能获得x的值5,没有内存区域存放x的名字"x",它在编译为汇编的过程中就被替换成了诸如[7C3F2DC2]的内存地址
正确的操作是在dbg函数中增加名字这一参数,但这太麻烦了,而且一旦我们更改了代码中的变量名,我们也必须修改参数

模板/宏的例子: C++的模板

模板可以看作增加一个预处理过程,如:模板 -> 代码 -> 运行

我们以C++为例,并假设编译过程可以简化为 C++代码 -> C语言 -> 指令 ,即认为C++是C语言的"生成器"

template
T f(T x)
{
	return x+x;
}

int main()
{
	cout << f(5)   << endl;
	cout << f("5") << endl;
}

它编译为C语言后会将模板函数T f(T x)据调用的类型生成为具有实际参数类型的代码
比如这里f被调用了两次,一次是int,一次是string,则会对应两种类型生成两种f函数

int f_int(int x)
{
	return x + x; //数值相加
}
int f_str(string x)
{
	return x + x; //字符串连接
}

int main()
{
	cout << f_int(5)   << endl;
	cout << f_str("5") << endl;
}

在执行过程中,会输出

10
55

利用模板输出变量名称

#include 

using namespace std;

#define dbg(x) (cout<< #x <<" = " << x << endl)

int main()
{
	int  n = 5;
	char m = 'p';
	dbg(n); // 输出 "n = 5"
	dbg(m); // 输出 "m = p"
	return 0;
}

它编译为

#include 

using namespace std;

int main()
{
	int  n = 5;
	char m = 'p';
	cout<< "n" << " = " << n << endl; // 输出 "n = 5"
	cout<< "m" << " = " << m << endl; // 输出 "m = p"
	return 0;
}

可以看到,编译器替我们完成了对m和n的名字的"复制粘贴",从而做到了的输出函数名字的功能

反射的例子: Kotlin

反射则可以看作程序执行时可以修改代码: 代码 <=> 运行

Kotlin中也支持C++的那种写法,不过称之为"泛型",也没有C++那种强大复杂的功能
作为替代,Kotlin发挥了动态语言的优势,使用反射来实现元编程,反射是程序运行时(Runtime)可以访问它本身状态或行为的一种能力。

Kotlin的反射做法

在Kotlin中输出变量名的一种实现是这样的

// test.kts
fun dbg(x : KProperty0)
{
    println("${x.name} = ${x.get()}")
}

val n=5
dbg(::n)

原理大致为: ::运算符在编译时根据输入的变量生成一个KProperty0类,在运行时,将包含n的反射信息的::n对象作为参数传递给dbg函数输出
正是因为编译和运行时的双重作用,导致其有很多限制,比如

  • n必须是不可变的
  • n不能在函数中

反射与模板在获取变量名过程中的区别

从上两个例子可以看出,模板产生的信息是存在于源代码中的(程序段),而反射是通过创建一个新的变量于内存中(数据段)
模板更加高效而反射更加灵活,但它们都是元编程的一种实现方式,都做到了让程序认识自己不过一个是先验的一个是后验的

有趣的事情

汇编也是支持反射的,只需修改CS:IP段存储的指令数据即可

MOV CS:[IP+2], 想修改的指令