宏病毒专杀cleanmacro(在C 中使用宏的一些实用经验总结)
一个众所周知的事实是-宏是很糟糕的,宏是一个历史的遗留的产物,已经无法很好的适应现代c 的发展。当然,有也一些宏是也是很不错的。
每条规则的背后都是有例外的,所以不要轻易的说“禁止使用宏”,虽然有一些宏可能会另代码看起来很不舒服(让人困惑),但还是有一些宏可以让显著地提升代码的可读性和正确性。
一个很糟糕的宏: max
宏有很多缺点,其中一个是宏是没有作用域的,这也就是如果一个文件中定义了一个宏,如header.hpp中场景了一个#define指令,那么该文件后续所有行的代码都会受到该宏的影响,直接或间接include该头文件的文件也一样。
void innerFunc(){#define MACRO_IN_FUNCTION 1 }// 我们可以在超出函数作用域的地方继续使用该宏#ifdef MACRO_IN_FUNCTION// TODO something.#endif
如在上面的代码中我们在函数内部分定义了一下宏MACRO_IN_FUNCTION,那么这函数定义之后的所有代码中都可以使用该宏,即是超出了函数的作有域,也就是说你不能将宏限制在一个函数,namesapce,或者类中。
考虑下面一个例子
#define max(a,b) (a < b) ? b : aint x = 42;int y = 43;int z = max(x, y); std::cout << x << 'n' << y << 'n' << z << 'n';
这个代码的输出是多少呢?毫无疑问,输出应该是:
424343
好的,下面我们来稍微改变一下我们的代码
int x = 42;int y = 43;int z = max( x, y);std::cout << x << 'n' << y << 'n' << z << 'n';
从语法上来讲,就是一段合理的代码,在语义上我们期望的结果应当是x是43,y和z是44,然而我们得到的结果是:
434545
为什么会是这个结果呢?当考虑到宏所做的事情后,这个结果却是正确的:宏只是简单的进行文本替换,如何让查看编译器进行宏展开后的结果呢,本人使用g ,只需要在g 命令中添加‘-E’即可,即g -E *.cpp,下面就是宏展开后的代码:
int x = 42;int y = 43;int z = ( x < y) ? y : x;std::cout << x << 'n' << y << 'n' << z << 'n';
从展开后的结果中可以看出,最大的值y,有两次的自增( )操作。基于文本的替换的宏在与C 进行结合时,可能会产生非常危险的混合,例如,如果你在其它文件中定义了函数max,然而你是无法调用到的,预处理器(preprocessor)会首先将其按宏的方式进行展开。
除此之外,宏还有很多其它问题,例如无法进行断点调试等等。虽然宏有很多问题,但在很多情况下宏却可以用来提升代码的质量。
1. 用于连接两个C 特性
C 具有非常丰富的特性,但是在一些高级的设计,多个部分之间无法做到无缝连接。
例如,我们需要在库中实现Accept并将此函数注入到应用程序的DocElement层次结构中。可惜,c 没有这样的直接机制。也有使用虚拟继承的变通方法,但虚函数的调用具有一定的性能开销。我们可以定义一个宏,并要求visitable层次结构中的每个类在类定义中使用该宏。但是,当我们在代码中使用宏时,需要保持代码的可控性,Andrei Alexandrescu提供了一些指导意见-定义宏的一个最重要的规则是让它尽可能少地自己执行,并尽可能快地将其转发给“真正的”实体(函数、类),也即是宏的逻辑足够简单。
#define DEFINE_VISITABLE() virtual ReturnType Accept(BaseVisitor& guest) { return AcceptImpl(*this, guest); }
2. 利用宏来减少冗余
在写代码时,如果一段相同的代码需要键入多次,那么使用宏可以减少这种冗余,并让代码看起来足够赏心悦目。
在现代C 中我们可能需要经常用到std::forward来传递左值或右值引用:
#define DEFINE_VISITABLE() virtual ReturnType Accept(BaseVisitor& guest) { return AcceptImpl(*this, guest); }
此模板代码中的&&表示值可以是l-value或r-value引用,具体取决于它们绑定的值是l-value还是r-value。std:forward允许将此信息传递给g。
但这需要很多代码来表达,每次输入都很麻烦,而且在阅读时还会占用一些空间。所以,为了简洁起见,我们可以定义一个宏来完成这个事情:
#define FWD(...) ::std::forward(__VA_ARGS__)
这样我们的代码看起来就是这要的:
templatevoid f(MyType&& myValue, MyOtherType&& myOtherValue){ g(FWD(myValue), FWD(myOtherValue));}
显然,这段代码比原始代码更加直观,更具有可读性。
3 宏可以带来更低层级的多态机制
宏可以被用于多态,但这只是多态的一个特殊情况,需要在编译阶段前被resolved,也就是发生在预处理阶段。
这是怎么做到的呢?您可以定义以-D开头的编译参数,并且可以使用代码中的#ifdef指令测试这些参数的存在性。根据它们的存在,您可以使用不同的#define来为代码中的表达式赋予不同的含义。
至少有两种信息你可以通过这种方式传递给你的程序:
允许系统调用代码可移植的操作系统类型(UNIX vs Windows)
可用的c 版本(c 98、c 03、c 11、c 14、c 17等)。
在设计用于不同项目的库代码中,让代码知道c 的版本是很有用的。它为库代码提供了灵活性,使其能够在可用的情况下编写高效的代码实现。
在使用c 高级特性的库中,如果库必须处理某些编译器bug,那么传递有关编译器本身及其版本的信息也是有意义的。这是Boost中的一个常见实践方式。
无论哪种方式,对于与环境或语言相关的指令,您都希望将这种检查保持在尽可能低的级别,并将其深深封装在实现代码中。
总结
在C 代码中烂用宏是一个很evil的事情,理解宏的机制,合理适合宏往往能带来非常大的收益,例如本人最近的工作中,需要将C 对象导致到js层,使用宏就大大提升了工作效率。