当宏间接扩展自身时,了解C的预处理器的行为
当我在一个充满宏技巧和魔法的大项目中工作时,我偶然发现了一个错误,其中宏没有正确扩展。结果输出是“ 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)" 在第一种情况下是正确的扩展,而在其他情况下却不是?
虽然这是很容易找到的资源是什么C预处理器做(和一些魔术,你可以用它做),我还没有找到一个解释如何它这样做,我想借此机会更好地了解如何预处理器完成它的工作以及它在扩展宏时使用的规则。
因此,鉴于此:预处理器决定将最终宏扩展为“ 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)
现在,知道C 预处理器不是图灵完备的事实,上述内容不可能扩展到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 步扫描该空序列并且不执行任何操作
- step 3 插入宏体
GLUE(A,B) - 第 4 步扫描
GLUE(A,B)宏,找到GLUE. 然而,它被抑制了,所以它保持原样。
- 第 1 步(嵌套)获取
- 所以
X第 2 步之后的最终值是GLUE(A,B)(注意,由于我们不在第 4 步GLUE,理论上,它不再被抑制) - 第 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).
所以我们的问题是:为什么?
回答
宏扩展是一个复杂的过程,只有通过了解发生的步骤才能真正理解。
-
当带有参数的宏被识别(宏名称标记后跟
(标记)时,)扫描并拆分(在,标记上)以下直到匹配的标记。发生这种情况时不会发生宏扩展(因此,s 和)必须直接出现在输入流中,不能出现在其他宏中)。 -
每个宏参数的宏体名列不是由preceeded
#或##或其次##是“预扫描”的宏扩大-完全的自变量中的任何宏将代入宏体前递归扩展。 -
生成的宏参数标记流被替换到宏的主体中。
#或##操作中涉及的参数被修改(字符串化或粘贴)并基于来自步骤 1 的原始解析器标记进行替换(步骤 2 不会发生这些)。 -
再次扫描生成的宏主体令牌流以查找要扩展的宏,但忽略当前正在扩展的宏。此时,输入中的更多标记(在步骤 1 中扫描和解析的内容之后)可以作为任何已识别宏的一部分包含在内。
重要的是发生了两种不同的递归扩展(上面的第 2 步和第 4 步),并且只有第 4 步中的一个会忽略同一宏的递归宏扩展。步骤 2 中的递归扩展不会忽略当前宏,因此可以递归扩展它。
因此,对于上面的示例,让我们看看会发生什么。对于输入
EXPAND(TEST PARENTHESIS())
- 步骤1中看到的宏
EXPAND参数列表并扫描TEST PARENTHESIS()了X - 第 2 步不识别
TEST为宏(不跟随(),但识别PARENTHESIS:- 第 1 步(嵌套)获取参数的空标记序列
- 第 2 步扫描该空序列并且不执行任何操作
- 步骤 3 插入到宏体中
(),结果就是:() - 第 4 步扫描
()宏并没有找到任何
- 所以
X第 2 步之后的最终值是TEST () - 第 3 步将其插入身体,给出
TEST () - 步骤 4 抑制
EXPAND并扫描步骤 3 的结果以获得更多宏,发现TEST- 第 1 步获取参数的空序列
- 第 2 步什么都不做
- 步骤 3 代入身体给予
EXPAND(0) - 第 4 步递归扩展它,抑制
TEST. 此时,EXPAND和TEST都被抑制了(因为在第 4 步展开),所以什么也没有发生
你的另一个例子EXPAND(TEST())是不同的
- step 1
EXPAND被识别为宏,并被TEST()解析为参数X - 第 2 步,这个流被递归解析。请注意,由于这是第 2 步,
EXPAND因此不被禁止- 步骤 1
TEST被识别为带有空序列参数的宏 - 第 2 步——什么都没有(空令牌序列中没有宏)
- 第3步,代入身体给予
EXPAND(0) - 第 4 步,
TEST被抑制并递归扩展结果- 第 1 步,
EXPAND被识别为一个宏(记住,此时仅TEST被第 4 步递归抑制——EXPAND在第 2 步递归中,所以不被抑制)0作为它的参数 - 第 2 步,
0被扫描,没有任何反应 - 第3步,代入身体给予
0 - 第 4 步,
0再次扫描宏(再次没有任何反应)
- 第 1 步,
- 步骤 1
- 第3步,将
0作为参数替换X到第一个的正文中EXPAND - 第 4 步,
0再次扫描宏(再次没有任何反应)
所以这里的最终结果是 0
- @LuizMartins: I'm pretty sure the answer comes down to [6.10.3.4 paragraph 4](http://port70.net/~nsz/c/c11/n1570.html#6.10.3.4p4): "There are cases where it is not clear whether a replacement is nested or not... Strictly conforming programs are not permitted to depend on such unspecified behavior." Basically, macro replacement semantics are underspecified. This is a known issue, and trying to pin down the exact semantics is futile.
回答
对于这种情况,宏替换有三个相关步骤:
- 对参数执行宏替换。
- 用它的定义替换宏,用参数替换参数。
- 重新扫描结果以进行进一步替换,同时取消替换的宏名称。
在EXPAND(TEST PARENTHESIS()):
- 步骤1中,宏替换是对参数进行
EXPAND,TEST PARENTHESIS():TEST后面没有括号,所以它不被解释为宏调用。PARENTHESIS()是一个宏调用,所以执行了三个步骤: 参数为空,所以没有对它们进行处理。然后PARENTHESIS()由 代替()。然后()重新扫描,没有找到宏。- 步骤 1 完成,我们有
EXPAND(TEST ()). (TEST ()不会重新扫描,因为它不是任何宏替换的结果。)
- 第 2 步,
EXPAND(TEST ())替换为TEST ()。 - 第 3 步,
TEST ()在抑制的同时重新扫描EXPAND:- 第1步,参数为空,所以没有对它们进行处理。
- 第 2 步,
TEST ()替换为EXPAND(0)。 - 第 3 步,
EXPAND(0)重新扫描,但EXPAND被抑制。
在EXPAND(TEST ()):
- 步骤 1,对 的参数执行宏替换
EXPAND:- 第1步,参数为
TEST空,所以没有处理。 - 第 2 步,
TEST ()替换为EXPAND(0)。 - 第三步,这个替换被重新扫描,
EXPAND(0)替换为0。
- 第1步,参数为
- 第 2 步,
EXPAND(TEST ())已成为EXPAND(0),EXPAND(0)并由 代替0。 - 第 3 步,
0为进一步的宏重新扫描,但没有。
问题中的其他示例也类似。归结为:
- 在 中
TEST PARENTHESIS(),缺少括号后TEST导致它在处理封闭宏调用的参数时不会被扩展。 PARENTHESIS扩展时在它后面加上括号,但这是在TEST扫描之后,并且在处理参数期间不会重新扫描。- 在封闭宏被替换后,
TEST重新扫描并随后被替换,但是此时封闭宏的名称被抑制。