C++的杂七杂八:宏(Macro)的各种玩法

/ 5评 / 2

Published by orzz.org(). (http://orzz.org/cxx-macro-play/)

宏(Macro)在C++中是一种非常犀利的工具。如果玩得好了,甚至可以把C++变得很奇怪。。


一、普通玩法

1. 基本功能

定义一个宏来替换字符串:

这样我们使用MACRO_PRINTF就如同printf一样。

我们还可以像这样定义一个宏名称,但不写任何东西:

然后像这样通过预定义的开关来调整代码实现:

我们可以像这样定义一个类似函数的宏:

但是像这样定义的宏定义时需要小心。
比如假如我们定义了一个比较大小的宏如下:

像这样使用是不会有问题的:

但是若是这样使用,就可能会有性能问题:

因为我们的func(10)在宏展开中写了两次,因此func内部的循环也执行了两次。

遇到这种情况,函数式的宏就不如真正的函数好用了,比如写一个泛型的大小比较函数:

2. 参数的字符串化和拼接

我们可以像这样把一个宏变成一个字符串:

然后宏在碰到#的时候,后面的x就不会被展开。因此这样写可以让任何输入的x都变成字符串。

为了让x展开,我们需要把宏嵌套一层:

宏在解析的时候,碰到第二层宏,会先展开x,再传递下去。
我们通过MACRO_EXPAND就可以得到x展开之后的样子,并把它变成字符串。

通过这个技巧,可以让我们在程序的运行时观察一个宏展开后的样子:

我们还可以把宏输入的两个参数直接连接起来:

同样,想要让参数展开后再连接,我们需要这样:

(在vc里想完全展开参数里的宏,必须在MACRO_EXPAND外面再嵌套一层,否则)

3. 变参宏

C99编译器标准里描述了可变参数的宏,目前主流的编译器也都支持它(gcc还另有一套可变参数宏写法,不过实际用起来大同小异)。
它的语法差不多是这个样子:

一般使用起来大概像这样个样子:

若__VA_ARGS__为空,printf会被展开成这样:printf(fmt, )。为了解决这个问题,gcc里的写法是在__VA_ARGS__前面写上##:

这样若变参为空时,附带着逗号也会被自动干掉。
vc里对这种写法也支持,不过就按一般的写法(不加##)也不会有问题。vc的编译器在处理变参宏时,若变参为空,会自动删除前面的逗号。

4. __FILE__、__LINE__、__FUNCTION__、...

这些玩意是编译器内置的宏定义。他们分别是:

__FILE__:当前源文件名
__LINE__:当前源代码行号
__FUNCTION__:当前的函数名
__DATE__:当前的编译日期
__TIME__:当前编译时间
__STDC__:当要求程序严格遵循ANSI C标准时该标识被赋值为1

等等。
gcc编译器还支持其他的一些,比如:

__PRETTY_FUNCTION__:当前的函数完整的声明(包含返回值参数表之类)

这样的话,我们可以利用它们写一个调试输出用的宏:


二、文艺玩法

1. 检查函数执行结果

可以这样用:

若判断错误,则输出函数名加参数以供调试。

还可以在执行成功后让它做后续动作:

2. switch case

用下面的写法可以让switch语句自动输出调试信息,而且还可以省略break:

3. 语法糖

比如实现一个简单的for_each:

在这里面我们可以根据类型的不同重载出特定的限制函数来扩充FOR_EACH的功能,也可以使用模板来实现“type i = 0”的自动推导。

4. 简化重复的代码

比如,我们可以通过下面的宏简单方便的为类增加一个属性:

使用方法:


三、奇葩玩法

1. 获得宏参数的个数

想要玩出这种效果,是需要动一下脑筋的。。
宏是“死”的,真正意义上的死代码。它没办法像模板那样推导,没办法像函数那样重载,没办法玩递归(编译器发现宏自身的嵌套,会停止展开下一层宏),甚至可变参数对宏来说,都只是一整块无法区分的符号。

那么让我们抛开任意多个参数自动判断的变态想法,来专注实现个数限定下的参数判断吧。
假设我们的参数个数最多不会超过10个,然后有下面的宏MACRO_ARGS:

想要计算出__VA_ARGS__里面有多少个参数,最好能够利用宏自身区分参数的机制。
比如我们可以再定义一个宏,然后把它们俩套起来:

但是目前FILTER里什么都没有,当CONTER把参数分散放入FILTER时,我们必须要让FILTER能够计算出传入参数的个数才行。
这一步的思维跳跃是比较难思考到的:

通过CONTER给FILTER默认10个参数,这时__VA_ARGS__若有1个以上的参数,那么FILTER里的_N就会自动被挤到(9, 8, 7, 6, 5, 4, 3, 2, 1, 0)中合适的位置上。

这样的写法有一个小缺点,就是当参数为空时,CONTER仍然返回1。
想想就知道为什么了:__VA_ARGS__为空时FILTER里的逗号并不会被消掉。
为了让它消掉逗号,在gcc里需要这样写:

可以测试一下:

看看它的返回结果是不是4。
当需要更多参数个数的判断时,我们只需要拓展FILTER和CONTER里的数字队列就行了。

上面的写法是gcc下的,在vc里稍有不同:vc编译器在处理__VA_ARGS__的时候,会直接把__VA_ARGS__里的所有内容(包括逗号)作为一整个参数传入下面一个宏里。也就是说,在vc编译器里__VA_ARGS__是不会被展开成参数列表传入第二层的,而是变成了一个大“参数”。

为了让vc能够把__VA_ARGS__打开,我们可以在FILTER的外部包一层什么都不做的宏:

这样的写法就可以同时支持gcc和vc了。

但是其实还有一个小问题。在vc中实际测试的时候可以发现,当CONTER什么都不写的时候,它仍然返回的是1。
这是因为在vc中,虽然在__VA_ARGS__不存在时会自动去掉前面的逗号,但是若是将它作为一个参数传递给下面一层宏的时候,逗号却会被保留下来。
vc不比gcc,可以通过##强制让编译器去掉这个逗号,因此这个小问题在vc里基本无解了。
但是不要紧,一般我们也不会像“CONTER()”这样去调用。至少,现在已经能够成功的自动计算10个以下的参数个数了。

2. 自动生成重复的代码

我们需要实现一个这样的功能:输入一个数字n和一个标识符x,然后得到一个把x连续重复了n次的字符串。
宏定义可以看起来像这个样子:

我们可以先从最简单的做起:当n为0或1时应该是个什么样子。
这个很简单,我们很快可以写出下面的代码:

那么接着,当n为2、3、4的时候呢?
我们可以套用前面的宏,把它们一个个连起来:

现在我们有了一堆宏了,下面就需要想办法让MACRO_N能够自动的调用它们:

下面我们可以用printf("%sn", MACRO_EXPAND(MACRO_N(3, str)));来试一试,屏幕上会输出“strstrstr”。

单看这种宏似乎没有什么大用处,但是在一些特殊场景下,这种玩法可以帮我们节省大量的代码。
比如说,在实现仿函数模板的时候,我们会需要“变参模板”的功能,因为一个仿函数模板根本不知道需要支持的函数会有多少个参数。由于目前的主流编译器暂时还不支持C++11标准里的变参模板,因此我们可能需要像下面这样写:

这代码。。实在是太蛋疼了。

有了前面那样的技术手段,我们可以先把参数自动拓展的功能玩出来。这需要先把前面那个宏变得更加通用化一些。
考虑到“typename T1, typename T2, typename T3”这样的符号串,必定在开头或结尾,会有一个符号和其他的不同(因为没有分隔用的逗号),通用化一些的宏应该需要再增加一个参数,来代表开头或结尾的特殊串。
这里实现一个以开头作为特殊串的自动化“Repeat”宏(当然,也可以尝试以结尾作为特殊串):

为了实现我们最初的目标,需要预先定义两个待重复的标记并定义一个用来做特殊处理的宏:

当调用TYPENAME_N(3)的时候,就可以得到“typename T1, typename T2, typename T3”。

现在,可以写一个“自动写代码的宏”了:

3. 伪变参模板

通过上面的一些手段,我们实现了“任意拓展参数个数”之类的功能,但当参数不同的时候,我们还是需要写不同的模板名来调用功能。
那么如何实现一个像FUNCTOR(...)这样的宏,可以自动根据参数个数选择不同的模板呢?

其实有了前面的第一个计算参数个数的技巧,这样的宏很容易写出来:

注意这里不能直接写成这个样子:

因为MACRO_ARGS_CONTER在vc下,没有参数的时候始终会返回1,而这里需要0。

但是这个代码还是太累赘了,写到FUNCTOR_9的时候不得不写9个参数。让我们把每个FUNCTOR_的定义都变成一样:

这样我们就可以轻松的拓展支持参数的个数,现在使用Functor可以非常愉快:

Published by orzz.org(). (http://orzz.org/cxx-macro-play/)

  1. 陈林熙说道:

    有没有什么办法获取一个宏参数的串长度的办法呢?
    如MC_CNT(abc)=3

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据