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

这里是这个系列的第二篇。这次我们开始关注一些更复杂的宏特性————object-like 宏的递归展开。

obj-like 的递归展开

在替换列表中出现的宏会被展开,这一过程将递归的进行下去,且是深度优先的。例如:

1
2
3
4
5
6
7
8
9
10
#define foo foz bar
#define bar 123
#define foz baz
#define baz 1
foo
-> foz bar
-> baz bar
-> 1 bar
-> 1 123

可以看到,当一个宏完全展开后,下一个宏才会被展开。但是,如果只有这一条规则那么很容易出现无限递归的情况。例如:

1
2
3
4
5
6
7
#define foo bar
#define bar foo
foo
-> bar
-> foo
-> 无限循环

因此在标准中对宏中涉及自指的部分做了限制:

If the name of the macro being replaced is found during this scan of the replacement list (not including the rest of the source file’s preprocessing tokens), it is not replaced. Furthermore, if any nested replacements encounter the name of the macro being replaced, it is not replaced. These nonreplaced macro name preprocessing tokens are no longer available for further replacement even if they are later (re)examined in contexts in which that macro name preprocessing token would otherwise have been replaced.

16.3.4 cpp.recanISO n3690

从字面含义理解很简单,主要表达了两点:

  1. 在展开的过程中,如果替换列表中出现了被展开宏,那么该被展开宏不会被展开。
  2. 更进一步的,在展开的过程中,任何嵌套的展开过程中出现了被展开宏,该被展开宏也不会被展开。

听起来很绕不是么,这属于典型的,说起来绕但是实现起来简单。没关系,我们可以这样理解。每次展开的时候会创建一个「蓝色集合」(一般将标记过的 token 称作 painted-blue),这个蓝色集合由本次展开的父级展开的蓝色集合加上当前展开的宏组成。然后每次对替换列表进行扫描的时候,所有在当前蓝色集合中的宏都不会被展开。挺起来还是很绕的话,我们来看一个实际展开的例子:

1
2
3
4
5
#define foo foo a bar b bar baz c
#define bar foo 12
#define baz bar 13
foo

定义三个宏 foo, bar, baz,展开过程如下图所示:

obj_like_recur.png

展开步骤为:

  1. 对于宏 foo 的展开,一开始蓝色集合是空集,没有禁止展开的宏名。所以我们对 foo 进行展开。展开结果是 foo a bar b bar baz c,每次展开都会创建一个新的蓝色集合,该蓝色集合继承自父级的蓝色集合并添加本次展开的宏名。于是新的蓝色集合就是空集+foo
  2. 逐个 token 向后检查1,此时蓝色集合为{foo},所以第一个 foo 不展开。a不是一个宏,放着不管就行。bar不在蓝色集合里,展开为foo 12。展开的同时创建新的蓝色集合{foo, bar}
  3. bar 展开的结果继续展开。此时的蓝色集合为{foo, bar}foo 在集合内,不展开。12保持不变。到此 bar完全展开,回退至上一层。注意,展开的顺序是,当一个宏完全展开后,才会去展开下一个宏,这一点很重要。
  4. b 不变。处理bar。注意,每一层展开的蓝色集合是不变的,和子展开无关。此时蓝色集合为{foo}。而不是{foo bar}。所以 bar 可以继续展开。展开过程和上一个 bar 一致,略去。
  5. 处理bazbaz 不在蓝色集合里,展开为bar 13 创建蓝色集合{foo baz}
  6. 展开bar。注意,此处蓝色集合中并没有 bar,所以bar可以继续展开。因为蓝色集合只继承自己的父级,和其他的无关。展开过程与之前的 bar 展开过程一致,略去。但需注意和之前不同的是,展开后蓝色集合变为{foo, baz, bar}如果 bar 中有baz,则 baz不会继续展开。

最终,展开的结果是foo a foo 12 b foo 12 foo 12 13 c
大家可以动手自己写几个例子试试看。虽然比较绕,但是掌握了以后还是很直白的。

本节内容到此结束。为了方便说明本节在介绍展开过程的时候略去了一些细节,这些细节将在下一节中交代。下一节我们来学习更加烧脑的 function-like 宏的递归展开。


  1. 1.实际操作过程稍有不同,不过此处这么理解并不会产生问题。具体的差别在下一节中将会交代。