前些日子,论坛里大打口水仗的时候,有人提出这样一个论断:模板本质上是宏。于是,诸位高手为此好好辩论了一番。我原本也想加入论战,但是觉得众人的言论已经覆盖了我的想法,所以也就作罢了。
尽管没有参与讨论,但“模板究竟和宏有什么关系”这个问题,始终在我的脑海中上下翻飞。每当我能够放松下来的时候,这个问题便悄悄地浮现。(通常都是哄儿子睡下,然后舒舒服服地冲个热水澡的时候)。
我思索了半天,决定做些实际的代码,以了解两者的差异。现在,我把试验的结果提交给大家,让众人来评判。
模板和宏是完全两个东西,这一点毋庸置疑。模板的一些功能,宏没有;宏的一些功能,模板没有。不可能谁是谁的影子。我们这里主要想要弄清的是,模板的本质究竟是不是宏。
需要明确一下,所谓“本质”的含义。这里我假定:一样东西是另一样东西的“本质”,有么后者是前者的子集,要么后者是通过前者直接或间接地实现的,要么后者的基础原理依赖于前者。如果哪位对此设定心存疑议,那么我们就得另行讨论了。
首先,我编写了一个模板,然后试图编写一个宏来实现这个模板的功能:
cls_tmpl<Tp1>ct;
使用宏的版本,这么写:
cls_mcr(Tp1)cm;
两者写法一样。但是下列代码便出现问题:
cls_tmpl<Tp1>ct1;
cls_tmpl<Tp1>ct2;
ct1=ct2;//Ok,ct1和ct2是同样的类型
cls_mcr(Tp1)cm1;
cls_mcr(Tp1)cm2;
cm1=cm2;//编译错误,cm1和cm2的类型不同
由于cls_mcr(Tp1)两次展开时,各自定义了一遍类,编译器会认为他们是两个不同的类型。但模板无论实例化多少次,只要类型实参相同,就是同一个类型。
这些便说明,模板和宏具备完全不同的语义,不可能用宏直接实现模板。如果要使宏避开这些问题,必须采用两阶段方式操作:
typedef cls_mcr(Tp1)cls_mcr_Tp1_;
cls_mcr_Tp1_cm1;
cls_mcr_Tp1_cm2;
cm1=cm2;//同一个类型,可以赋值
这反倒给了我们一个提示,或许编译器可以在一个“草稿本”上把宏展开,然后通过用展开后的类名将所有用到的cls_mcr(…)替换掉。这样便实现了模板。
但事情并没有那么简单。请考虑以下代码:
ct1.f2();//编译错误,Tp1不包含成员函数g()
这种机制的目的主要是为了减少编译时间。但后来却成为了泛型编程和模板元编程中非常重要的一个机制。(最早用于traits等方面,参见《C++ Template》一书。我在模拟属性的尝试中,也使用了这种机制,很好用。)
相反,宏是直接将所有的代码同时展开,之后在编译过程中执行全面的语言检查,无论其成员函数使用与否。而模板一开始仅作语法检查,只有使用到的代码才做语义检查和实际编译。
从这一点看出,即使允许宏在“草稿本”中展开,它同模板在展开方式上也存在着巨大的差别。仅凭这一点,便可以否定“模板的本质是宏”这个论断。但是,如果我们把眼光放宽一些,是否可以这么认为:尽管模板和宏采用了完全不同的展开方式,那么如果模板中的每个成员都看作独立的宏,那么是否可以认为模板是通过一组宏,而不是一个宏,实现的呢?
让我们来看模板cls_tmpl<>的成员函数f1():
但是,当我们考虑另一个问题,事情就不再那么简单了。请看以下代码:
x=y;
a=b;
假设x、y、a、b都是int类型。这两行代码编译后可能会变成如下等效的汇编代码(实际上是机器码):
mov eax, y
mov x, eax
mov eax, b
mov a, eax
我们可以看到,这两行代码分别转化成两条汇编指令,所不同的是参与的内存变量。可以认为编译器把赋值的汇编码(机器码)做成一个“宏”:
#define assign(v1, v2) \
mov eax, v2\
mov v1, eax
在编译时用内存变量(的地址)替换“宏”的参数。那么这种情况下,我们是否应该认为编译器(或者说编译)的本质是宏呢?
由于C++标准没有规定用什么方式展开模板,而我们也很难知道各种编译器是如何实现模板的,也就无从得知模板是否通过宏物理实现。但是,我个人的看法是,宏和模板都是语法层面的机制。如果一定要用宏这种语法层面的机制,来解释模板的(物理)本质,那也太牵强附会了。
模板和宏是完全两个东西,这一点毋庸置疑。模板的一些功能,宏没有;宏的一些功能,模板没有。不可能谁是谁的影子。我们这里主要想要弄清的是,模板的本质究竟是不是宏。
需要明确一下,所谓“本质”的含义。这里我假定:一样东西是另一样东西的“本质”,有么后者是前者的子集,要么后者是通过前者直接或间接地实现的,要么后者的基础原理依赖于前者。如果哪位对此设定心存疑议,那么我们就得另行讨论了。
首先,我编写了一个模板,然后试图编写一个宏来实现这个模板的功能:
template<typename T>
class cls_tmpl
{
public:
string f1()
{
strings=v.f()+”1000”;
return s;
}
void f2()
{
v.g();
}
private:
T v;
};
下面是宏的模拟:
#define cls_mcr(T) \
class \
{\
public:\
void f1() {\
v.f();\
}\
void f2() {\
v.g();\
}\
private:\
T v;\
}
当我使用模板时,需要这么写:
cls_tmpl<Tp1>ct;
使用宏的版本,这么写:
cls_mcr(Tp1)cm;
两者写法一样。但是下列代码便出现问题:
cls_tmpl<Tp1>ct1;
cls_tmpl<Tp1>ct2;
ct1=ct2;//Ok,ct1和ct2是同样的类型
cls_mcr(Tp1)cm1;
cls_mcr(Tp1)cm2;
cm1=cm2;//编译错误,cm1和cm2的类型不同
由于cls_mcr(Tp1)两次展开时,各自定义了一遍类,编译器会认为他们是两个不同的类型。但模板无论实例化多少次,只要类型实参相同,就是同一个类型。
这些便说明,模板和宏具备完全不同的语义,不可能用宏直接实现模板。如果要使宏避开这些问题,必须采用两阶段方式操作:
typedef cls_mcr(Tp1)cls_mcr_Tp1_;
cls_mcr_Tp1_cm1;
cls_mcr_Tp1_cm2;
cm1=cm2;//同一个类型,可以赋值
这反倒给了我们一个提示,或许编译器可以在一个“草稿本”上把宏展开,然后通过用展开后的类名将所有用到的cls_mcr(…)替换掉。这样便实现了模板。
但事情并没有那么简单。请考虑以下代码:
class Tp1
{
public:
string f()
{
return“X”;
}
};
cls_tmpl<Tp1>ct1;
ct1.f1();
cls_mcr(Tp1)cm1;//编译错误:Tp1不包含成员函数g()
cm1.f1();
尽管模板和宏的代码一样,但是编译器却给出了不同的结果。回溯到cls_tmpl和cls_mcr的定义,两者都有一个f2()成员函数访问了Tp1的成员函数g()。但是,模板的代码并没有给出任何错误,而宏却有编译错误。要解释清楚这个差异,先得了解一下C++模板的一个特殊的机制:模板中的代码只有在用到时才会被实例化。也就是说,当遇到cls_tmpl<Tp1>时,编译器并不会完全展开整个模板类。只有当访问了模板上的某个成员函数时,才会将成员函数的代码展开作语义检查。所以,当我仅仅调用f1()时,不会引发编译错误。只有在调用f2()时,才会有编译错:
ct1.f2();//编译错误,Tp1不包含成员函数g()
这种机制的目的主要是为了减少编译时间。但后来却成为了泛型编程和模板元编程中非常重要的一个机制。(最早用于traits等方面,参见《C++ Template》一书。我在模拟属性的尝试中,也使用了这种机制,很好用。)
相反,宏是直接将所有的代码同时展开,之后在编译过程中执行全面的语言检查,无论其成员函数使用与否。而模板一开始仅作语法检查,只有使用到的代码才做语义检查和实际编译。
从这一点看出,即使允许宏在“草稿本”中展开,它同模板在展开方式上也存在着巨大的差别。仅凭这一点,便可以否定“模板的本质是宏”这个论断。但是,如果我们把眼光放宽一些,是否可以这么认为:尽管模板和宏采用了完全不同的展开方式,那么如果模板中的每个成员都看作独立的宏,那么是否可以认为模板是通过一组宏,而不是一个宏,实现的呢?
让我们来看模板cls_tmpl<>的成员函数f1():
string f1()
{
strings=v.f()+”1000”;
return s;
}
如果我们把f1看作一个宏, f1在需要时以宏的方式展开,然后正式编译。当然,我们首先必须将模板转换成一组宏。如果哪个编译器真是这样做的,那么可以勉强地认为这个编译器是通过宏实现模板的。(不过这种样子的“宏”,还能算宏吗?)
但是,当我们考虑另一个问题,事情就不再那么简单了。请看以下代码:
x=y;
a=b;
假设x、y、a、b都是int类型。这两行代码编译后可能会变成如下等效的汇编代码(实际上是机器码):
mov eax, y
mov x, eax
mov eax, b
mov a, eax
我们可以看到,这两行代码分别转化成两条汇编指令,所不同的是参与的内存变量。可以认为编译器把赋值的汇编码(机器码)做成一个“宏”:
#define assign(v1, v2) \
mov eax, v2\
mov v1, eax
在编译时用内存变量(的地址)替换“宏”的参数。那么这种情况下,我们是否应该认为编译器(或者说编译)的本质是宏呢?
由于C++标准没有规定用什么方式展开模板,而我们也很难知道各种编译器是如何实现模板的,也就无从得知模板是否通过宏物理实现。但是,我个人的看法是,宏和模板都是语法层面的机制。如果一定要用宏这种语法层面的机制,来解释模板的(物理)本质,那也太牵强附会了。
我觉得比较合理的解释是:如果一定要把宏和模板扯上什么“亲戚关系”,那么说宏是模板的远方大表哥比较合理。两者在技术上有一定的同源性。都是以标识符替换为基础的。但是,其他在方面,我们很难说它们有多大的相似性或者关系。宏是语法层面的机制,而模板则深入到语义层面。无论是语法、语义,还是具体的实现,都没有什么一样的地方。
故事本该就此结束,但是这个说法却越传越广。我猜想原因有可能两种。其一是为了使一些初学者理解模板的基本特征,用宏来近似地解释以下模板,使人容易理解。我曾经对一些不开窍的同僚说:“如果你实在搞不清模板,可以把它理解成象宏那样的东西。但是记住,它跟宏没关系!”很多人话只听半句。他们记住了前半句,扔掉了更重要的后半句。所以,我现在再也不说这样的话了。
另一种原因可就险恶多了。一些试图打压C++的人总是不遗余力地贬损C++的各种特性,(C++的问题我们得承认,但是总得实事求是吧),特别是那些最强大的功能。而模板则是首当其冲的。如果把模板和宏,这种丑陋的、臭名昭著的“史前活化石”联系在一起,对于打击C++的名声有莫大的帮助。(即便C++社群,也非常积极地排斥宏)。
但是,很多人却对模板大做文章,想借此说明模板在本质上是落后的东西。以此欺骗世人,特别是那些懵懂的初学者。我写此文的目的,就是实在忍受不了这种指鹿为马的言论,借此反击一下。