被误解的C++——模板和宏

原文出处:http://topic.csdn.net/u/20070620/08/086a1126-b980-44c9-ab0b-9552e94c8e4b.html


前些日子,论坛里大打口水仗的时候,有人提出这样一个论断:模板本质上是宏。于是,诸位高手为此好好辩论了一番。我原本也想加入论战,但是觉得众人的言论已经覆盖了我的想法,所以也就作罢了。 
尽管没有参与讨论,但“模板究竟和宏有什么关系”这个问题,始终在我的脑海中上下翻飞。每当我能够放松下来的时候,这个问题便悄悄地浮现。(通常都是哄儿子睡下,然后舒舒服服地冲个热水澡的时候:))。 
我思索了半天,决定做些实际的代码,以了解两者的差异。现在,我把试验的结果提交给大家,让众人来评判。 
模板和宏是完全两个东西,这一点毋庸置疑。模板的一些功能,宏没有;宏的一些功能,模板没有。不可能谁是谁的影子。我们这里主要想要弄清的是,模板的本质究竟是不是宏。 
需要明确一下,所谓“本质”的含义。这里我假定:一样东西是另一样东西的“本质”,有么后者是前者的子集,要么后者是通过前者直接或间接地实现的,要么后者的基础原理依赖于前者。如果哪位对此设定心存疑议,那么我们就得另行讨论了。 
首先,我编写了一个模板,然后试图编写一个宏来实现这个模板的功能: 
template <typename   T> 
class   cls_tmpl 

public: 
string   f1()   { 
string s=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()   { 
string s=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++标准没有规定用什么方式展开模板,而我们也很难知道各种编译器是如何实现模板的,也就无从得知模板是否通过宏物理实现。但是,我个人的看法是,宏和模板都是语法层面的机制。如果一定要用宏这种语法层面的机制,来解释模板的(物理)本质,那也太牵强附会了。 
我觉得比较合理的解释是:如果一定要把宏和模板扯上什么“亲戚关系”,那么说宏是模板的远方大表哥比较合理。两者在技术上有一定的同源性。都是以标识符替换为基础的。但是,其他在方面,我们很难说它们有多大的相似性或者关系。宏是语法层面的机制,而模板则深入到语义层面。无论是语法、语义,还是具体的实现,都没有什么一样的地方。 
至于“模板的本质是宏”这种说法的始作俑者,可能是Stroupstrup本人。最初他提出模板(当时称为类型参数)可以通过宏实现。但是不久以后,便发现他心目中的模板和宏有着天壤之别。于是,他和其他C++的创建者一起建立和发展了模板的各种机制。 
故事本该就此结束,但是这个说法却越传越广。我猜想原因有可能两种。其一是为了使一些初学者理解模板的基本特征,用宏来近似地解释以下模板,使人容易理解。我曾经对一些不开窍的同僚说:“如果你实在搞不清模板,可以把它理解成象宏那样的东西。但是记住,它跟宏没关系!”很多人话只听半句。他们记住了前半句,扔掉了更重要的后半句。所以,我现在再也不说这样的话了。 
另一种原因可就险恶多了。一些试图打压C++的人总是不遗余力地贬损C++的各种特性,(C++的问题我们得承认,但是总得实事求是吧),特别是那些最强大的功能。而模板则是首当其冲的。如果把模板和宏,这种丑陋的、臭名昭著的“史前活化石”联系在一起,对于打击C++的名声有莫大的帮助。(即便C++社群,也非常积极地排斥宏)。 
实际上,模板的本质是不是宏,根本没有什么实际意义。即便是这样,也丝毫不会影响模板的价值。很多高级的编程机制都是建立在传统的技术之上的,比如虚函数就是利用函数指针表和间接调用实现的。从没有人拿这一点说事。 
但是,很多人却对模板大做文章,想借此说明模板在本质上是落后的东西。以此欺骗世人,特别是那些懵懂的初学者。我写此文的目的,就是实在忍受不了这种指鹿为马的言论,借此反击一下。 
另一方面,通过模板和宏的特性的比较,可以使我们更深入地了解和理解两种机制的特性、能力和限制。温故而知新,总会有新的收获。 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值