Lui*_*ins 77 c macros c-preprocessor
当我在一个充满宏技巧和魔法的大项目中工作时,我偶然发现了一个错误,其中宏没有正确扩展。结果输出是“ 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)
Run Code Online (Sandbox Code Playgroud)
4 行。
雪上加霜的是,几乎对宏的任何修改都会使它们正常工作:
#define EXPAND(X) X
#define PARENTHESIS() ()
#define TEST() EXPAND(0)
// Manually replaced PARENTHESIS()
EXPAND(TEST ()) // 0
Run Code Online (Sandbox Code Playgroud)
#define EXPAND(X) X
#define PARENTHESIS() ()
#define TEST() EXPAND(0)
// Manually replaced TEST()
EXPAND(EXPAND(0)) // 0
Run Code Online (Sandbox Code Playgroud)
// Set EXPAND to 0 instead of X
#define EXPAND(X) 0
#define PARENTHESIS() ()
#define TEST() EXPAND(0)
EXPAND(TEST PARENTHESIS()) // 0
Run Code Online (Sandbox Code Playgroud)
但最重要的是,最奇怪的是,下面的代码以完全相同的方式失败:
#define EXPAND(X) X
#define PARENTHESIS() ()
#define TEST() EXPAND(0)
EXPAND(EXPAND(EXPAND(EXPAND(TEST PARENTHESIS())))) // EXPAND(0)
Run Code Online (Sandbox Code Playgroud)
这意味着预处理器完全有能力扩展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)
Run Code Online (Sandbox Code Playgroud)
现在,知道C 预处理器不是图灵完备的事实,上述内容不可能扩展到A B。如果是这样的话,GLUE会扩大MACRO,MACRO会扩大GLUE。这将导致无限递归的可能性,可能意味着 Cpp 的图灵完整性。对于那里的预处理器向导来说,可悲的是,上面的宏不会扩展是一个保证。
失败并不是真正的问题,真正的问题是:在哪里?预处理器决定在哪里停止扩展?
分析步骤:
EXPAND参数列表并扫描GLUE(MACRO, ())了XGLUE(MACRO, ())为宏:
MACRO和()作为参数MACRO ()GLUE并扫描MACRO ()宏,发现MACRO
GLUE(A,B)GLUE(A,B)宏,找到GLUE. 然而,它被抑制了,所以它保持原样。X第 2 步之后的最终值是GLUE(A,B)(注意,由于我们不在第 4 步GLUE,理论上,它不再被抑制)GLUE(A,B)EXPAND并扫描GLUE(A,B)更多的宏,找到GLUE( uuh )
A和B用于参数(哦不)A B(嗯...)A B宏,但什么也没找到A B这将是我们的梦想。可悲的是,宏扩展为GLUE(A,B).
所以我们的问题是:为什么?
Chr*_*odd 50
宏扩展是一个复杂的过程,只有通过了解发生的步骤才能真正理解。
当带有参数的宏被识别(宏名称标记后跟(标记)时,)扫描并拆分(在,标记上)以下直到匹配的标记。发生这种情况时不会发生宏扩展(因此,s 和)必须直接出现在输入流中,不能出现在其他宏中)。
每个宏参数的宏体名列不是由preceeded#或##或其次##是“预扫描”的宏扩大-完全的自变量中的任何宏将代入宏体前递归扩展。
生成的宏参数标记流被替换到宏的主体中。#或##操作中涉及的参数被修改(字符串化或粘贴)并基于来自步骤 1 的原始解析器标记进行替换(步骤 2 不会发生这些)。
再次扫描生成的宏主体令牌流以查找要扩展的宏,但忽略当前正在扩展的宏。此时,输入中的更多标记(在步骤 1 中扫描和解析的内容之后)可以作为任何已识别宏的一部分包含在内。
重要的是发生了两种不同的递归扩展(上面的第 2 步和第 4 步),并且只有第 4 步中的一个会忽略同一宏的递归宏扩展。步骤 2 中的递归扩展不会忽略当前宏,因此可以递归扩展它。
因此,对于上面的示例,让我们看看会发生什么。对于输入
EXPAND(TEST PARENTHESIS())
Run Code Online (Sandbox Code Playgroud)
EXPAND参数列表并扫描TEST PARENTHESIS()了XTEST为宏(不跟随(),但识别PARENTHESIS:
(),结果就是:()()宏并没有找到任何X第 2 步之后的最终值是TEST ()TEST ()EXPAND并扫描步骤 3 的结果以获得更多宏,发现TEST
EXPAND(0)TEST. 此时,EXPAND和TEST都被抑制了(因为在第 4 步展开),所以什么也没有发生你的另一个例子EXPAND(TEST())是不同的
EXPAND被识别为宏,并被TEST()解析为参数XEXPAND因此不被禁止
TEST被识别为带有空序列参数的宏EXPAND(0)TEST被抑制并递归扩展结果
EXPAND被识别为一个宏(记住,此时仅TEST被第 4 步递归抑制——EXPAND在第 2 步递归中,所以不被抑制)0作为它的参数0被扫描,没有任何反应00再次扫描宏(再次没有任何反应)0作为参数替换X到第一个的正文中EXPAND0再次扫描宏(再次没有任何反应)所以这里的最终结果是 0
Eri*_*hil 12
对于这种情况,宏替换有三个相关步骤:
在EXPAND(TEST PARENTHESIS()):
EXPAND,TEST PARENTHESIS():
TEST 后面没有括号,所以它不被解释为宏调用。PARENTHESIS()是一个宏调用,所以执行了三个步骤: 参数为空,所以没有对它们进行处理。然后PARENTHESIS()由 代替()。然后()重新扫描,没有找到宏。EXPAND(TEST ()). (TEST ()不会重新扫描,因为它不是任何宏替换的结果。)EXPAND(TEST ())替换为TEST ()。TEST ()在抑制的同时重新扫描EXPAND:
TEST ()替换为EXPAND(0)。EXPAND(0)重新扫描,但EXPAND被抑制。在EXPAND(TEST ()):
EXPAND:
TEST空,所以没有处理。TEST ()替换为EXPAND(0)。EXPAND(0)替换为0。EXPAND(TEST ())已成为EXPAND(0),EXPAND(0)并由 代替0。0为进一步的宏重新扫描,但没有。问题中的其他示例也类似。归结为:
TEST PARENTHESIS(),缺少括号后TEST导致它在处理封闭宏调用的参数时不会被扩展。PARENTHESIS扩展时在它后面加上括号,但这是在TEST扫描之后,并且在处理参数期间不会重新扫描。TEST重新扫描并随后被替换,但是此时封闭宏的名称被抑制。在阅读了克里斯·多德的精彩回答并花了一些时间思考后,我想我已经解决了这个问题。
如果您像正常人一样使用 C 预处理器,那么这里确实没有什么问题需要避免。只要不要制作互相提及的宏就可以了。然而,如果您涉足黑暗艺术,您会发现上面的问题非常容易被发现。那么在这里,我将解释如何避免它。
请注意,我不会解释为什么会在这里发生这些情况(克里斯的回答已经很好地解释了原因),但我将列出它们发生的位置以及如何解决它们。
当您的宏“调用”具有推迟/间接扩展时,可能会出现此问题。我所说的“间接”是指不能当场进行的扩展,而只能在一些串联或替换/替换之后进行。为了理解它,我们首先看一个安全的例子:
#define EXPAND(MACRO) MACRO
#define MY_MACRO(A, B) A##B
EXPAND(MY_MACRO(1,2)) // 12
Run Code Online (Sandbox Code Playgroud)
这里,MY_MACRO(1,2)是对宏的直接引用,并且具有要按原样扩展的适当参数。因此,在 中进行任何替换之前,预处理器将立即EXPAND扩展它。
现在让我们将其与这些示例进行比较:
#define EXPAND(MACRO, PAREN_ARGS) MACRO PAREN_ARGS
#define MY_MACRO(A, B) A##B
EXPAND(MY_MACRO, (1,2)) // 12
Run Code Online (Sandbox Code Playgroud)
#define EXPAND(MACRO) MACRO (1,2)
#define MY_MACRO(A, B) A##B
EXPAND(MY_MACRO) // 12
Run Code Online (Sandbox Code Playgroud)
请注意,里面的参数EXPAND(“ MY_MACRO, (1,2)”和“ MY_MACRO”)“看起来不像宏”。尽管我们希望它们在最后完全扩展,但它们的格式不正确,无法在参数中立即扩展。虽然现在这些工作正常,但由于没有相互引用,它们的扩展将推迟到替换之后EXPAND,这可能会带来问题。
每当参数中的宏无法按原样展开时,它就不能在其展开中的任何点包含“被调用者”。
我们可以通过添加对上面示例的相互引用来证明这一点:
#define EXPAND(MACRO, PAREN_ARGS) MACRO PAREN_ARGS
// Now MY_MACRO references EXPAND
#define MY_MACRO(A, B) EXPAND(A,B)
// Fails to expand
EXPAND(MY_MACRO, (1,2)) // EXPAND(1,2)
^ ^
| |
| This guy is postponed...
|
...so it cannot eventually expand to this guy.
Run Code Online (Sandbox Code Playgroud)
#define EXPAND(MACRO) MACRO (1,2)
// Now MY_MACRO references EXPAND
#define MY_MACRO(A, B) EXPAND(A)
// Fails to expand
EXPAND(MY_MACRO) // EXPAND(1)
^ ^
| |
| This guy is postponed...
|
...so it cannot eventually expand to this guy.
Run Code Online (Sandbox Code Playgroud)
另一方面,我们的示例没有间接扩展,是完全安全的并且按预期进行评估:
#define EXPAND(MACRO) MACRO
// Now MY_MACRO references EXPAND
#define MY_MACRO(A, B) EXPAND(A)
// Succeeds
EXPAND(MY_MACRO(1,2)) // 1
^ ^
| |
| This guy can expand right here...
|
...so it can freely expand to this guy.
Run Code Online (Sandbox Code Playgroud)
由于MY_MACRO(1,2)它是一个实际的宏,因此可以立即对其进行评估,并且不会出现任何问题。
如果我的判断是正确的,那么您不必担心任何先前的“被调用者”,只需担心直接将“推迟的宏”作为参数的“被调用者”。您也不必担心其他参数,因为预处理器也会在替换之前尝试完全扩展它们。此外,如果不需要宏的完全扩展,这不会造成问题。
但是,如果您希望参数中的宏完全扩展并且其扩展将是间接的,请仔细检查它。