【发布时间】:2021-06-10 02:45:35
【问题描述】:
当我在处理一个充满宏技巧和魔法的大型项目时,我偶然发现了一个宏无法正确扩展的错误。结果输出是“EXPAND(0)”,但EXPAND被定义为“#define EXPAND(X) X”,所以显然输出应该是“0”。
“没问题”,我心想。 “这可能是一些愚蠢的错误,这里有一些令人讨厌的宏,毕竟有很多地方会出错”。正如我所想的那样,我将行为不端的宏隔离到他们自己的项目中,大约 200 行,并开始研究 MWE 以查明问题。 200 行变成了 150,然后又变成了 100,然后是 20、10……令我震惊的是,这是我最后的 MWE:
#define EXPAND(X) X
#define PARENTHESIS() ()
#define TEST() EXPAND(0)
EXPAND(TEST PARENTHESIS()) // EXPAND(0)
4 行。
雪上加霜,几乎任何对宏的修改都会使它们正常工作:
#define EXPAND(X) X
#define PARENTHESIS() ()
#define TEST() EXPAND(0)
// Manually replaced PARENTHESIS()
EXPAND(TEST ()) // 0
#define EXPAND(X) X
#define PARENTHESIS() ()
#define TEST() EXPAND(0)
// Manually replaced TEST()
EXPAND(EXPAND(0)) // 0
// Set EXPAND to 0 instead of X
#define EXPAND(X) 0
#define PARENTHESIS() ()
#define TEST() EXPAND(0)
EXPAND(TEST PARENTHESIS()) // 0
但最重要也是最奇怪的是,下面的代码以完全相同的方式失败:
#define EXPAND(X) X
#define PARENTHESIS() ()
#define TEST() EXPAND(0)
EXPAND(EXPAND(EXPAND(EXPAND(TEST PARENTHESIS())))) // EXPAND(0)
这意味着预处理器完全能够扩展EXPAND,但由于某种原因,它绝对拒绝在最后一步再次扩展它。
现在,我将如何在我的实际程序中解决这个问题既不存在也不存在。虽然一个解决方案会很好(即一种将令牌 EXPAND(TEST PARENTHESIS()) 扩展为 0 的方法),但我最感兴趣的是:为什么?为什么 C 预处理器得出的结论是“EXPAND(0)”在第一种情况下是正确的扩展,而在其他情况下却不是?
虽然很容易在 what 上找到 C 预处理器的资源(以及一些您可以使用它的 magic),但我还没有找到一个可以解释的它是如何做到的,我想借此机会更好地了解预处理器是如何工作的,以及它在扩展宏时使用了哪些规则。
因此,鉴于此:预处理器决定将最终宏扩展为“EXPAND(0)”而不是“0”的原因是什么?
编辑:在阅读了 Chris Dodd 非常详细、合乎逻辑且措辞恰当的答案后,我做了任何人在相同情况下都会做的事情……试着想出一个反例:)
我炮制的是这个不同的 4-liner:
#define EXPAND(X) X
#define GLUE(X,Y) X Y
#define MACRO() GLUE(A,B)
EXPAND(GLUE(MACRO, ())) // GLUE(A,B)
现在,知道the C preprocessor is not Turing complete 的事实,以上内容不可能扩展到A B。如果是这种情况,GLUE 将扩展 MACRO,MACRO 将扩展 GLUE。这将导致无限递归的可能性,可能意味着 Cpp 的图灵完备性。可悲的是,对于那里的预处理器向导来说,上面的宏不扩展是一种保证。
失败并不是真正的问题,真正的问题是:在哪里?预处理器在哪里决定停止扩展?
分析步骤:
- 步骤 1 看到宏
EXPAND并在参数列表GLUE(MACRO, ())中扫描X - 步骤 2 将
GLUE(MACRO, ())识别为宏:- 第 1 步(嵌套)获取
MACRO和()作为参数 - 第 2 步扫描它们但没有找到宏
- 第 3 步插入宏体产生:
MACRO () - 第 4 步抑制
GLUE并扫描MACRO ()以查找宏,找到MACRO- 第 1 步(嵌套)获取参数的空标记序列
- 第 2 步扫描该空序列并且不执行任何操作
- 第三步插入宏体
GLUE(A,B) - 第 4 步扫描
GLUE(A,B)中的宏,找到GLUE。然而,它被抑制了,所以它就这样离开了。
- 第 1 步(嵌套)获取
- 所以
X在第2步之后的最终值为GLUE(A,B)(请注意,由于我们不在GLUE的第4步,理论上它不再被抑制了) - 第 3 步将其插入正文,给出
GLUE(A,B) - 第 4 步抑制
EXPAND并扫描GLUE(A,B)以获取更多宏,找到GLUE(uuh)- 第 1 步获取
A和B作为参数(哦,不) - 第 2 步对它们没有任何作用
- 第 3 步替换成身体,给出
A B(嗯...) - 第 4 步扫描
A B中的宏,但什么也没找到
- 第 1 步获取
- 那么最后的结果就是
A B
这将是我们的梦想。遗憾的是,宏扩展为GLUE(A,B)。
所以我们的问题是:为什么?
【问题讨论】:
-
我喜欢这个问题(以及你是怎么问的)。我不会在这里提交答案,但我对“递归”函数式宏解析的想法非常怀疑。您期望两个宏生成看起来像另一个类似函数的宏的文本,然后期望它本身被评估的事情似乎......太多了
-
这可能属于第 6.10.3.4/p4 条的规定,其中说,“在某些情况下,不清楚替换是否是嵌套的。” 然后以“严格遵守的程序不允许依赖于这种未指定的行为。”
-
@user3386109 确实6.10.3.4 描绘了一幅相当不错的画面:"[...] 重新扫描生成的预处理标记序列 [...],以便替换更多宏名称。如果在替换列表的扫描过程中找到被替换的宏的名称 [...],它不会被替换。此外,如果任何嵌套替换遇到被替换的宏的名称被替换了,它没有被替换。”...
-
..."这些未替换的宏名称预处理标记不再可用于进一步替换,即使它们稍后在该宏所在的上下文中(重新)检查否则名称预处理令牌将被替换。”
-
将数百行代码转换为由六行或更少行组成的 MWE 应该并不少见。
标签: c macros c-preprocessor