C 预处理器能否将嵌套宏展开停在 m2(1)?
可以。对于这组宏,第二行保留为 m2(1) 是允许的结果,因为嵌套替换的边界存在歧义。GCC 和 Clang 将它展开为 1,并不意味着保留 m2(1) 不符合标准。这里依据的是题目引用的 N3783 工作草案。N3783,6.10.5.5 与 J.1
目前没有核实到会对这段代码产生 m2(1) 的具体实现,以及对应的版本和运行模式,因此无法指定某款编译器。
标准留下了什么选择空间?
宏替换完成参数代入后,还需要重扫描替换结果,寻找可以继续展开的宏。重扫描也会结合源文件中后续的 preprocessing tokens,因此一次宏调用可能由两部分拼成:宏名来自上一次替换,参数表来自后面的源代码。
宏正在被替换时,如果直接或通过嵌套替换再次遇到该宏名,需要抑制它的展开。因这种抑制而没有展开的宏名,在后续扫描中也不会重新获得展开资格。
当调用横跨替换列表和后续源代码时,后一次替换是否仍算嵌套在前一次替换中?N3783 的 6.10.5.5 第 4 段展示了这种歧义:
#define f(a) a*g
#define g(a) f(a)
f(2)(9)
它允许产生以下任一种结果:
2*f(9)
2*9*g
该示例明确说明,严格符合标准的程序不能依赖这种未指定行为,也为本题中的两种解释提供了依据。N3783,6.10.5.5 第 4 段
为什么第一行停在 m1(0)?
第一行可以按下面的过程理解,箭头表示预处理推导步骤:
m1(m2,m1)(0)
→ m2(m1)(0)
→ m1(0)
展开外层 m1 后,替换列表成为 m2(m1)。在这个替换列表中展开 m2,得到宏名 m1。
这个 m1 位于外层 m1 的嵌套替换中,因此被抑制。后面遇到 (0) 时,已被抑制的 token 也不会重新获得展开资格,最终保留:
int x = m1(0);
为什么第二行可能得到两种结果?
第二行先经历以下替换:
m2(m1)(m2,1)
→ m1(m2,1)
→ m2(1)
最初的 m2(m1) 产生宏名 m1,它与源文件后续的 (m2,1) 组成新的调用。
如果展开这个新调用时,最初的 m2 已经可以再次展开,那么最后一步会继续进行:
m2(1)
→ 1
这对应题目给出的 GCC 和 Clang 结果。GCC 的实现文档说明,它通过展开上下文管理宏的禁用状态,并在对应上下文退出时重新启用宏。GCC 宏展开实现说明
另一种解释是,后续 m1(m2,1) 的替换仍嵌套在最初的 m2 替换中。按这种解释,再次产生的 m2 会被抑制,结果停在:
int y = m2(1);
备选结果涉及的是该次替换的嵌套关系,并不意味着一个宏在整行代码中只能展开一次。
如何核对具体实现?
将原始示例保存为 macro.c,分别查看预处理输出:
gcc -E -P macro.c
clang -E -P macro.c
比较其他实现时,需要同时记录版本、语言模式以及是否启用了传统预处理模式。某个版本的输出只能证明该版本在对应配置下的行为,不能代替标准依据,也不能推广到所有版本。
这里讨论的是预处理 token 序列。保留下来的 m1(0) 或 m2(1) 不会自动成为合法的 C 表达式;后续编译能否通过,还取决于相关声明和使用位置。需要可移植结果时,应调整宏接口,避免依赖这种跨替换边界形成的递归调用。
备注:内容仅供参考。
