宏定义黑魔法-从入门到奇技淫巧 (3)

这是本系列的第三篇,也是整个系列中最核心的一篇。在这一节中将会介绍宏展开过程中最核心的机制,后续的很多技巧都是在这些特性上发展出来的。这一节的内容是 function-like 的宏展开。

func-like 的宏展开

func-like 的宏展开基本思路和 obj-like 的宏展开是一致的。但是由于参数的存在,所以多了若干额外的规则。这使得我们无法使用上一节所讲的那种漂亮的树状规则来描述展开过程。主要过程是这么几个步骤,除了有关参数的外,obj-like 的宏也遵循相同的步骤:

  1. identifier-list 也就是参数列表里的参数会被完全展开。但如果该参数在替换列表中被 ### 所调用,那么该参数不展开。
  2. 使用展开后的结果替换替换列表中的相关内容。
  3. 执行 ### 的结果,并替换相关内容。
  4. 将得到的新的替换列表重新扫描找到可替换的宏名并展开。
  5. 在整个过程中遵循上一节中提到的关于自指的规则。约束参数列表的起始蓝色集合与约束宏名的起始蓝色集合一致。

此外还有两个特性

  1. 如果一个 func-like 的宏的宏名后边没有参数列表(括号和 identifier-list)那么这个宏名将不被视作一个宏。
  2. 每次展开结束以后会向后看一个 token 是否能够与上次展开的结果形成一个 func-like 的宏。如果有就展开这个新形成的宏。

接下来,我们分析几个例子来解释上边的规则

1
2
3
4
5
6
#define FOO_ 0
#define FOO_1 1
#define PRIMITIVE_CAT(x, y) x y
PRIMITIVE_CAT(FOO_, 1)
-> 0 1

在这组宏中 FOO_ 作为参数会被先行展开为 0,然后在替换替换列表中相应的部分。

1
2
3
4
5
6
7
#define FOO_ 0
#define FOO_1 1
#define CAT(x, y) x ## y
CAT(FOO_, 1)
-> FOO_1
-> 1

类似的一组宏定义。这回 PRIMITIVE_CAT 中的参数 FOO_ 由于在替换列表中被 ## 所调用,所以并不会被展开,而是直接合并成一个 token FOO_1。并且在重扫描的阶段被展开为 1。注意,也就是说这里在一个宏中实际上存在两次扫描展开。

那么有的时候我们就是希望先展开参数然后再进行拼接呢?这是我们需要借助一个额外的宏间接的来做这件事情。我们稍稍修改下上边的宏定义

1
2
3
4
5
6
7
8
#define FOO_ 0
#define FOO_1 1
#define PRIMITIVE_CAT(x, y) x ## y
#define CAT(x, y) PRIMITIVE_CAT(x, y)
CAT(FOO_, 1)
-> PRIMITIVE_CAT(0, 1)
-> 01

这回由于 PRIMITIVE_CAT 的存在参数 FOO_ 在第一层中并没有被 ## 直接调用。所以,FOO_ 作为参数会被首先展开为 0。之后在 PRIMITIVE_CAT 中完成拼接。注意,这回的结果01之间并没有空格,因为他们被拼接成了一个 token。

接下来再看一个涉及自指的例子:

1
2
3
4
5
6
7
8
#define FOO(x, y) x + y
#define BAR(y) 13 + y
#define BAZ(z) FOO(BAZ(16), FOO(11, 0)) + z
BAZ(15)
-> FOO(BAZ(16), FOO(11, 0)) + 15
-> FOO(BAZ(16), 11 + 0) + 15
-> BAZ(16) + 11 + 0 + 15

在第一次展开 FOO 的时候,当前的蓝色集合为 {BAZ}。首先,先完全展开参数,此时展开第一个参数 BAZ 时,由于此时 BAZ 在蓝色集合中所以停止展开。第二个参数 FOO 则不在蓝色集合中,因此可以展开。注意,此时参数列表的蓝色集合与宏名的蓝色集合一致,FOO的参数中是可以继续展开FOO的。

但是,需要注意的一点是,虽然参数会先完全展开,然后替换替换列表中的对应部分。但参数展开后的结果在重扫描时仍然会沿用同一个蓝色集合。例如:

1
2
3
4
5
6
7
8
9
#define BAR() 1 BAZ()
#define BAZ() BAR
#define FOO(x) BAR() - x()
FOO(BAR())
-> FOO(1 BAR)
-> BAR() - 1 BAR()
-> 1 BAZ() - 1 BAR()
-> 1 BAR - 1 BAR()

注意,此处参数 BAR 展开为 1 BAR 以后就开始进行替换。替换完后,在重扫描时,发现后一个 BAR(),但是此时由于在参数完全展开时BAR()已经展开过,所以此时展开的蓝色集合中有 BAR。但是前一个BAR()的蓝色集合继承自FOO,所以并不受影响。因此,第一个BAR()可以展开,第二个不行。

然后再看一个更加复杂的涉及自指的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
#define FOO_(x) FOO_1(x)
#define FOO_1(x) FOO_2(x) + 1
#define FOO_2(x) FOO_1(x) - 1
#define BAR(x) FOO_ ## x (12) FOO_2
BAR(1) (5)
-> FOO_ ## 1 (12) FOO_2 (5)
-> FOO_1 (12) FOO_2 (5)
-> FOO_2(12) + 1 FOO_2 (5)
-> FOO_1(12) - 1 + 1 FOO_2 (5)
-> FOO_1(12) - 1 + 1 FOO_1(5) - 1
-> FOO_1(12) - 1 + 1 FOO_2(5) + 1 -1

  1. 展开宏 BAR(1)。将参数替换到位,形成 FOO_ ## 1 (12) FOO_2
  2. 重扫描替换结果展开 FOO_1(12) 得到 FOO_2(12) + 1
  3. 继续展开 FOO_2(12) 得到 FOO_1(12) - 1,此时的 FOO_1在蓝色集合中,不继续展开。至此 FOO_2(12) 完全展开为 FOO_1(12) - 1 返回
  4. FOO_1(12) 完全展开为 FOO_1(12) - 1 + 1
  5. BAR(1) 完全展开为 FOO_1(12) - 1 + 1 FOO_2
  6. 根据上边提到的特性 2。展开后的结果中最后一个 token FOO_2(5) 形成了一个 func-like 的宏。所以展开FOO_2 (5) 过程与之前雷同,略去。

整个展开过程越来越复杂,不过核心只需记住几点,1. 参数先展开。2. 替换后重扫描。3. 蓝色集合中不展开。4. #, ## 不展开。 5. 最后的括号要检查。

可变长参数 __VA_ARGS__

此外 func-like 宏还支持边长的参数。只需在参数列表的最后写上 ... 就能够使用 __VA_ARGS__ 来表示边长参数了。例如:

1
2
3
4
5
#define FOO(x, ...) __VA_ARGS__
FOO(1, (1, 2), (1,2,3))
-> (1, 2), (1,2,3)

本节的内容到此结束,枯燥的内容基本上讲完了。很多人可能感觉这些理论不是很必要,又绕有麻烦,大部分例子看起来也是生造出来的。这只是为了说明方便,下一节我们将利用目前学到的特性来实现些有用的东西,尽情期待下一节:宏的惯用法及实现图灵完备的宏。