这是本系列的第三篇,也是整个系列中最核心的一篇。在这一节中将会介绍宏展开过程中最核心的机制,后续的很多技巧都是在这些特性上发展出来的。这一节的内容是 function-like 的宏展开。
func-like 的宏展开
func-like 的宏展开基本思路和 obj-like 的宏展开是一致的。但是由于参数的存在,所以多了若干额外的规则。这使得我们无法使用上一节所讲的那种漂亮的树状规则来描述展开过程。主要过程是这么几个步骤,除了有关参数的外,obj-like 的宏也遵循相同的步骤:
- identifier-list 也就是参数列表里的参数会被完全展开。但如果该参数在替换列表中被
#
或##
所调用,那么该参数不展开。 - 使用展开后的结果替换替换列表中的相关内容。
- 执行
#
和##
的结果,并替换相关内容。 - 将得到的新的替换列表重新扫描找到可替换的宏名并展开。
- 在整个过程中遵循上一节中提到的关于自指的规则。约束参数列表的起始蓝色集合与约束宏名的起始蓝色集合一致。
此外还有两个特性
- 如果一个 func-like 的宏的宏名后边没有参数列表(括号和 identifier-list)那么这个宏名将不被视作一个宏。
- 每次展开结束以后会向后看一个 token 是否能够与上次展开的结果形成一个 func-like 的宏。如果有就展开这个新形成的宏。
接下来,我们分析几个例子来解释上边的规则
|
|
在这组宏中 FOO_
作为参数会被先行展开为 0,然后在替换替换列表中相应的部分。
|
|
类似的一组宏定义。这回 PRIMITIVE_CAT
中的参数 FOO_
由于在替换列表中被 ##
所调用,所以并不会被展开,而是直接合并成一个 token FOO_1
。并且在重扫描的阶段被展开为 1。注意,也就是说这里在一个宏中实际上存在两次扫描展开。
那么有的时候我们就是希望先展开参数然后再进行拼接呢?这是我们需要借助一个额外的宏间接的来做这件事情。我们稍稍修改下上边的宏定义
|
|
这回由于 PRIMITIVE_CAT
的存在参数 FOO_
在第一层中并没有被 ##
直接调用。所以,FOO_
作为参数会被首先展开为 0。之后在 PRIMITIVE_CAT
中完成拼接。注意,这回的结果01之间并没有空格,因为他们被拼接成了一个 token。
接下来再看一个涉及自指的例子:
|
|
在第一次展开 FOO
的时候,当前的蓝色集合为 {BAZ}
。首先,先完全展开参数,此时展开第一个参数 BAZ
时,由于此时 BAZ
在蓝色集合中所以停止展开。第二个参数 FOO
则不在蓝色集合中,因此可以展开。注意,此时参数列表的蓝色集合与宏名的蓝色集合一致,FOO
的参数中是可以继续展开FOO
的。
但是,需要注意的一点是,虽然参数会先完全展开,然后替换替换列表中的对应部分。但参数展开后的结果在重扫描时仍然会沿用同一个蓝色集合。例如:
|
|
注意,此处参数 BAR
展开为 1 BAR
以后就开始进行替换。替换完后,在重扫描时,发现后一个 BAR()
,但是此时由于在参数完全展开时BAR()
已经展开过,所以此时展开的蓝色集合中有 BAR
。但是前一个BAR()
的蓝色集合继承自FOO
,所以并不受影响。因此,第一个BAR()
可以展开,第二个不行。
然后再看一个更加复杂的涉及自指的例子:
|
|
- 展开宏 BAR(1)。将参数替换到位,形成
FOO_ ## 1 (12) FOO_2
- 重扫描替换结果展开
FOO_1(12)
得到FOO_2(12) + 1
- 继续展开
FOO_2(12)
得到FOO_1(12) - 1
,此时的FOO_1
在蓝色集合中,不继续展开。至此FOO_2(12)
完全展开为FOO_1(12) - 1
返回 FOO_1(12)
完全展开为FOO_1(12) - 1 + 1
BAR(1)
完全展开为FOO_1(12) - 1 + 1 FOO_2
- 根据上边提到的特性 2。展开后的结果中最后一个 token
FOO_2
与(5)
形成了一个 func-like 的宏。所以展开FOO_2 (5)
过程与之前雷同,略去。
整个展开过程越来越复杂,不过核心只需记住几点,1. 参数先展开。2. 替换后重扫描。3. 蓝色集合中不展开。4. #
, ##
不展开。 5. 最后的括号要检查。
可变长参数 __VA_ARGS__
此外 func-like 宏还支持边长的参数。只需在参数列表的最后写上 ...
就能够使用 __VA_ARGS__
来表示边长参数了。例如:
|
|
本节的内容到此结束,枯燥的内容基本上讲完了。很多人可能感觉这些理论不是很必要,又绕有麻烦,大部分例子看起来也是生造出来的。这只是为了说明方便,下一节我们将利用目前学到的特性来实现些有用的东西,尽情期待下一节:宏的惯用法及实现图灵完备的宏。