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

longshanks 2007-06-20 08:30:39
模板和宏
前些日子,论坛里大打口水仗的时候,有人提出这样一个论断:模板本质上是宏。于是,诸位高手为此好好辩论了一番。我原本也想加入论战,但是觉得众人的言论已经覆盖了我的想法,所以也就作罢了。
尽管没有参与讨论,但“模板究竟和宏有什么关系”这个问题,始终在我的脑海中上下翻飞。每当我能够放松下来的时候,这个问题便悄悄地浮现。(通常都是哄儿子睡下,然后舒舒服服地冲个热水澡的时候:))。
我思索了半天,决定做些实际的代码,以了解两者的差异。现在,我把试验的结果提交给大家,让众人来评判。
模板和宏是完全两个东西,这一点毋庸置疑。模板的一些功能,宏没有;宏的一些功能,模板没有。不可能谁是谁的影子。我们这里主要想要弄清的是,模板的本质究竟是不是宏。
需要明确一下,所谓“本质”的含义。这里我假定:一样东西是另一样东西的“本质”,有么后者是前者的子集,要么后者是通过前者直接或间接地实现的,要么后者的基础原理依赖于前者。如果哪位对此设定心存疑议,那么我们就得另行讨论了。
首先,我编写了一个模板,然后试图编写一个宏来实现这个模板的功能:
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++社群,也非常积极地排斥宏)。
实际上,模板的本质是不是宏,根本没有什么实际意义。即便是这样,也丝毫不会影响模板的价值。很多高级的编程机制都是建立在传统的技术之上的,比如虚函数就是利用函数指针表和间接调用实现的。从没有人拿这一点说事。
但是,很多人却对模板大做文章,想借此说明模板在本质上是落后的东西。以此欺骗世人,特别是那些懵懂的初学者。我写此文的目的,就是实在忍受不了这种指鹿为马的言论,借此反击一下。
另一方面,通过模板和宏的特性的比较,可以使我们更深入地了解和理解两种机制的特性、能力和限制。温故而知新,总会有新的收获。
...全文
3211 90 打赏 收藏 转发到动态 举报
写回复
用AI写文章
90 条回复
切换为时间正序
请发表友善的回复…
发表回复
wdmjj 2012-10-07
  • 打赏
  • 举报
回复
膜拜大婶们的讨论 学习C++中...
youyouting7 2011-10-26
  • 打赏
  • 举报
回复
原来是这样……谢谢啦
loops 2007-09-13
  • 打赏
  • 举报
回复
宏也挺好用的,模板也挺好用的,我经常用它们来解决程序中不同的问题。
现在总体上,比如以前用宏实现的函数,现在都转成模板来实现了,但是宏的其他功能,即预编译的字符串展开的功能,依旧是模板不可以代替的。
wishfly 2007-09-13
  • 打赏
  • 举报
回复
up
bbsdkjdx 2007-09-10
  • 打赏
  • 举报
回复
学习
yanzihan2004 2007-09-10
  • 打赏
  • 举报
回复
mark,回头再看~~~
red_berries 2007-09-10
  • 打赏
  • 举报
回复
mark
yuyunliuhen 2007-09-10
  • 打赏
  • 举报
回复
mark
xmoon1983 2007-09-10
  • 打赏
  • 举报
回复
mark 学习
浮生若梦 2007-09-10
  • 打赏
  • 举报
回复
原来是好老的帖子了
Minkey 2007-06-23
  • 打赏
  • 举报
回复
一直在Mark楼主的帖子,呵呵~~
BlueTrees 2007-06-22
  • 打赏
  • 举报
回复
拉姆达表达式可以定义一切可计算问题,理论上写程序只要写拉姆达表达式就可以了,

但是拉姆达表达式要规约为图灵机的计算比较罗嗦吧,复杂的拉姆达表达式编译起来估计很费时。计算效率也不知道如何。
BlueTrees 2007-06-22
  • 打赏
  • 举报
回复
循环和递归有着很大的不同。

要解递归为循环,必须有模拟栈保存状态,普通循环不需要栈也可以。

那个说宏递归,是用文件自我包含达成的,文件如果可以自我包含那就可以递归。
healer_kx 2007-06-22
  • 打赏
  • 举报
回复
强贴要留名,JUST SO So。
longshanks 2007-06-22
  • 打赏
  • 举报
回复
to fohoo(飞狐),Vitin(卫亭):
惭愧,一直没好好地研究vc ide。也是我没耐心去读msdn。这里谢过了。
longshanks 2007-06-22
  • 打赏
  • 举报
回复
to bqtiger(分针):
差不多,就像DelphiGuy说的,inline不是强制的,那么了解编译器的行为,对编程有很大的帮助。C++标准最恼人的就是把很多复杂问题扔给编译器处理,这样我们就不得不逐个分析编译器的行为,以免编程的时候弄巧成拙。
bqtiger 2007-06-22
  • 打赏
  • 举报
回复
呵呵。我猜楼主关心inline的问题,是想推测模板的实例化方式吧?:)
bqtiger 2007-06-22
  • 打赏
  • 举报
回复

  现在的编译器好象都倾向于分别展开分别编译这种方式,毕竟这样对c/l中任何一方的改动都很小,风险也就最小。
  不过我相信将来的主流会逐渐转移到template repository这种方式,好处是显而易见的。不降低代码生成质量的前提下大大提高编译速度。compiler vendors应该都在做这种研究了。他们中的任何一个都不会容忍编译速度超过自己几倍的竞争对手来挤压自己的市场份额。呵呵。这一天到来的时候,必定又是一场腥风血雨。
bqtiger 2007-06-22
  • 打赏
  • 举报
回复
关于模板的实例化方式。下面这篇文章会很有帮助
http://gcc.gnu.org/onlinedocs/gcc/Template-Instantiation.html
chzuping 2007-06-22
  • 打赏
  • 举报
回复
Mark
加载更多回复(70)

64,674

社区成员

发帖
与我相关
我的任务
社区描述
C++ 语言相关问题讨论,技术干货分享,前沿动态等
c++ 技术论坛(原bbs)
社区管理员
  • C++ 语言社区
  • encoderlee
  • paschen
加入社区
  • 近7日
  • 近30日
  • 至今
社区公告
  1. 请不要发布与C++技术无关的贴子
  2. 请不要发布与技术无关的招聘、广告的帖子
  3. 请尽可能的描述清楚你的问题,如果涉及到代码请尽可能的格式化一下

试试用AI创作助手写篇文章吧