元编程,模板与反射浅谈: 以获取变量名称为例
何为"元(meta)"
元(meta)的一种含义是"关于自身的" (Meta is a prefix meaning "referring to itself")
我们可以理解为 元-X = X的X
即 meta-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], 想修改的指令