如何获取可变参数宏实参的数量
在阅读一些开源代码的时候,可能会看到类似下面这种神秘的宏:
1 |
|
这些宏的作用是获取可变参数宏的实参数量。在调用 NARG
时,传入若干个参数,就会返回这些参数的数量,例如:
1 | int i = NARG(a); //i = 1 |
这些宏使用了精妙的技巧来实现这个功能。了解其实现原理的最好方法是将宏一步步展开,在这个过程中观察它究竟施展了什么样的“魔法”。为了方便理解,这里将 ARG_N
和 RSEQ_N
定义的数字数量精简成这样:
1 |
以 NARG(a, b, c)
为例,这个宏调用会被替换成:
1 | NARG_(a, b, c, RSEQ_N) |
再把 RSEQ_N
展开,变成:
1 | NARG_(a, b, c, 3, 2, 1, 0) |
再被替换成:
1 | ARG_N(a, b, c, 3, 2, 1, 0) |
对照参考 ARG_N
的定义,它总是会被替换成第4个参数,所以这个宏被替换成了3:
1 |
|
再以两个参数的调用 NARG(a, b)
为例,这个宏会被替换成2:
1 |
|
以此类推,可以总结出它的实现原理:将输入参数与一个倒序的数字序列拼接成新的参数序列,传递给 ARG_N
宏,这个宏输出固定位置的参数,这个位置上的数字刚好等于输入参数的个数。
这种方法有一定的局限性,比如它支持的最大参数数量是有限的,依赖 ARG_N
和 RSEQ_N
中定义的数量。在本文开头的例子中,它最多支持64个宏参数,假如要支持更多的参数,就需要在这两个宏里添加更多的数字。ARG_N
和 RSEQ_N
里的数字数量一定要精确匹配,否则这套机制就失效了。另外也顺带一提,ARG_N
里的 _1
、_2
、_3
等标示符只是一个参数占位符,没有其它作用,即使写成别的名字也是可以的,写成数字是为了更方便与RSEQ_N
里的数字对齐。
另外一个局限是,它不能支持0个参数,即使在RSEQ_N
里定义了0这个数字。原因是宏展开时只是单纯的文本替换,例如 NARG()
这个宏调用,会被展开成:
1 | NARG_(, 3, 2, 1, 0) |
即使没有输入参数,在它后面的逗号还是保留着的,所以实际上相当于有1个参数。在最后调用 ARG_N
的时候,会被替换成1:
1 |
|
最后,在使用MSVC编译器时,如果使用默认的编译选项,这套方法是不生效的,因为MSVC的宏参数展开规则不符合标准。简单地说,在MSVC中,当一个宏将它的可变参数传递给另外一个宏时,这些参数被作为一个整体来传递,而不是拆分成多个传递,所以对于另外一个宏来说,只接收到一个参数。例如, NARG(a, b, c)
这个宏调用,虽然从字面上它也会被展开成 ARG_N(a, b, c, 3, 2, 1, 0)
,但从语义上它却是这样的:
1 |
|
传递给 ARG_N
的参数,都一起被视为第一个参数了。所以在MSVC下编译会出现ARG_N
参数不足的警告,并且得到空的结果。
在MSVC添加 /Zc:preprocessor
编译选项,可以解决这个问题。这个编译选项改变了编译器展开宏的规则,使其更符合标准。在Visual Studio的项目设置中也可以修改这个选项:
除了修改编译器选项,还有另外一种取巧的方法,即在调用 ARG_N
之前,增加一个中间层调用,如下所示:
1 |
这里增加了一个新的 EXPAND
宏,这个宏从字面上没有实际用途,只是把参数原样保留。但是从语义上来说,这个宏改变了展开的行为。
仍然以 NARG(a, b, c)
为例,它的展开过程如下所示:
1 | NARG_(a, b, c, 3, 2, 1, 0) // 1 |
从第2步到第3步这个过程可以看出来,宏调用跟函数调用不一样,宏调用只是文本替换, EXPAND
只把 ARG_N(a, b, c, 3, 2, 1, 0)
原样保留,而不是先展开 ARG_N
再调用 EXPAND
。这是关键的地方,在第2步的时候,虽然 a, b, c, 3, 2, 1, 0
这些参数还是一个整体,但由于这时候还没有去展开 ARG_N
,所以不会像之前那样全部都当作第一个参数,它们只作为文本被保留下来。到了第3步,这时候就是正常的宏展开,参数可以一一对应。
增加 EXPAND
中间层对其它编译器没有影响,所以如果要编写跨平台的代码,应该使用这个方法。