概述
实_不_相_瞒[3]:宏是C和C++语言的抽象设施中最生硬的工具,它是披着函数外衣的饥饿的狼,很难驯服,它会我行我素地游走于各处。要避免使用宏。
讨论
很难找到足够绘声绘色的言语来描述宏,但我们还是要勉力为之。文献[Sutter04]§31中谈到:
由于几方面的原因,宏已经成为讨厌、恶心、杂乱的混合体,其中最主要的原因在于它们被吹捧为一种文本替换设施,其效果在预处理阶段就产生了,而此时C++的语法和语义规则都还没有起作用。
如果这还不够清楚,我们再引用一些Bjarne Stroustrup的论述:
我讨厌大多数形式的预处理器和宏。C++的目标之一就是使C的预处理器成为多余的(§4.4,§18),因为我认为其操作天生就容易出错。——[Stroustrup94]§3.3.1
在C++中几乎从不需要用宏。可以用const(§5.4)或者enum(§4.8)定义易于理解的常量[见第15条],用inline(§7.1.1)避免函数调用的开销[但是要见第8条],用template(第13章)指定函数系列和类型系列[见第64条至第67条],用namespace(§8.2)避免名称冲突(见第57条至第59条)。——[Stroustrup00]§1.6.1
关于宏的第一规则就是:不要使用它,除非不得不用。几乎每个宏都说明程序设计语言、程序或者程序员存在缺陷。——[Stroustrup00]§7.8
C++的宏的主要问题在于,它们表面上看起来很好,而实际上做的却是另一回事。宏会忽略作用域,忽略类型系统,忽略所有其他的语言特性和规则,而且会劫持它为文件其余部分所定义(#define)的符号。宏调用看上去很像符号或者函数调用,但实际上并非如此。宏不太“卫生”,也就是说,它会根据自己被使用时所处的环境引人注目而且令人惊奇地展开为各种东西。宏需要进行文本替换,因此编写远距离也正确的宏接近于一种魔法,而精通这种魔法既无意义又无趣味。
不少人都认为与模板相关的错误是最难以解读的,他们可能还没有看到误写和误用的宏所引起的那些错误。模板是C++类型系统的一部分,因此编译器可以更好地对它们进行处理,而宏天生是与语言本身割裂开来的,因此很难处理。更糟的是,与模板不同,宏可能展开为在偶然情况下能够编译的“传输线噪声”。最后,宏中的错误可能只有在宏展开之后才能被报告出来,而不是在定义时。
即使在极少的情况下,有正当理由编写宏(见例外情况),也决不要考虑编写一个以常见词或者缩略语为名字的宏。尽可能快地取消宏的定义(#undef),总是给它们取形如SCREAMING_ UPPERCASE_AND_UGLY这样明显的、大写的、而且难看的名字,并且不要将它们放在头文件中。
示例
例 将模板实例化传给宏。宏仅能理解C语言的小括号和方括号,并将其进行匹配。然而,C++又定义了一个新的括号结构,即模板中使用的尖括号<和>。宏无法正确地匹配它们,这意味着在下面的宏调用中:
MACRO( Foo<int, double> )
宏会认为传给自己的是两个参数,即Foo<int和double>,而事实上该结构是一个C++实体。
例外情况
宏仍然是几个重要任务的惟一解决方案,比如#include保护符(guard)(见第24条),条件编译中的#ifdef和#if defined,以及assert的实现(见第68条)。
在条件编译(如与系统有关的部分)中,要避免在代码中到处杂乱地插入#ifdef。相反,应该对代码进行组织,利用宏来驱动一个公共接口的多个实现,然后始终使用该接口。
如果不想到处复制和粘贴代码段,那么可以使用宏(但要非常小心)。
我们注意到,文献[C99]和[Boost]中分别含有对预处理器比较温和和比较激进的扩展。





