当我从事一个充满宏技巧和魔法的大型项目时,我偶然发现了一个宏无法正确扩展的错误。结果输出是“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),但我还没有找到解释如何的资源em> 它做到了,我想借此机会更好地了解预处理器是如何工作的,以及它在扩展宏时使用的规则。
因此,鉴于此:预处理器决定将最终宏扩展为“EXPAND(0)
”而不是“0
”的原因是什么?
编辑:在阅读了克里斯·多德(Chris Dodd)非常详细、合乎逻辑且措辞恰当的答案后,我做了任何人在相同情况下都会做的事情……试着想出一个反例:)
我炮制的是这个不同的 4 线:
#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 步扫描该空序列并且不执行任何操作 第 3 步插入宏主体 GLUE(A,B) 第 4 步扫描 GLUE( A,B) 对于宏,寻找 GLUE。然而,它被抑制了,所以它就这样离开了。
第 1 步(嵌套)获取 MACRO 和 () 作为参数
第 2 步扫描它们但没有找到宏
第 3 步插入宏体产生: MACRO ()
第 4 步抑制 GLUE 并扫描 MACRO () 中的宏,找到 MACRO 第 1 步(嵌套)为参数获取一个空标记序列 第 2 步扫描该空序列并且什么也不做 第 3 步插入宏主体 GLUE(A,B) 步骤4 扫描 GLUE(A,B) 中的宏,找到 GLUE。然而,它被抑制了,所以它就这样离开了。
第 1 步(嵌套)获取参数的空标记序列
第 2 步扫描那个空序列并且什么都不做
步骤 3 插入宏体 GLUE(A,B)
第 4 步扫描 GLUE(A,B) 中的宏,找到 GLUE。然而,它被抑制了,所以它就这样离开了。
所以第 2 步后 X 的最终值为 GLUE(A,B) (请注意,由于我们不在 GLUE 的第 4 步,理论上它不再被抑制)
第 3 步将其插入主体,给出 GLUE(A,B)
第 4 步抑制 EXPAND 并扫描 GLUE(A,B) 以获取更多宏,找到 GLUE (uuh) 第 1 步为参数获取 A 和 B(哦,不)第 2 步对它们不做任何事情 第 3 步替换为给出 AB 的主体(嗯...) 步骤 4 扫描 AB 以查找宏,但什么也没找到
第 1 步为参数获取 A 和 B(哦,不)
第 2 步对它们没有任何作用
第 3 步代入给 AB 的身体(嗯...)
第 4 步扫描 AB 以查找宏,但什么也没找到
那么最后的结果就是AB
这将是我们的梦想。遗憾的是,宏扩展为 GLUE(A,B)
。
所以我们的问题是:为什么?
宏扩展是一个复杂的过程,只有通过了解发生的步骤才能真正理解。
当一个带有参数的宏被识别时(宏名记号后跟(记号),后面的记号直到匹配的记号)被扫描和分割(上记号,记号)。发生这种情况时不会发生宏扩展(因此 ,s 和 )必须直接存在于输入流中,并且不能存在于其他宏中)。每个宏参数的名称出现在宏体中,其名称前面没有 # 或 ## 或后跟 ## 是“预扫描”的,以便宏展开 - 完全在参数中的任何宏都将在替换到宏体之前递归展开。生成的宏参数标记流被替换到宏的主体中。 # 或 ## 操作中涉及的参数会根据步骤 1 中的原始解析器标记进行修改(字符串化或粘贴)和替换(这些参数不会发生步骤 2)。再次扫描生成的宏主体令牌流以查找要扩展的宏,但忽略当前正在扩展的宏。此时,输入中的其他标记(在步骤 1 中扫描和解析的内容之后)可以作为识别的任何宏的一部分包括在内。
重要的是发生了两种不同的递归扩展(上面的第 2 步和第 4 步),并且只有第 4 步中的一个忽略了同一宏的递归宏扩展。步骤 2 中的递归扩展不会忽略当前宏,因此可以递归扩展它。
因此,对于上面的示例,让我们看看会发生什么。对于输入
EXPAND(TEST PARENTHESIS())
步骤 1 看到宏 EXPAND 并在参数列表 TEST PARENTHESIS() 中扫描 X
第 2 步不将 TEST 识别为宏(没有后面的 (),但可以识别 PARENTHESIS:第 1 步(嵌套)为参数获取一个空标记序列 第 2 步扫描该空序列并且什么也不做 第 3 步插入宏主体 ( ) 产生这样的结果: () 步骤 4 扫描 () 中的宏并且没有找到任何
第 1 步(嵌套)获取参数的空标记序列
第 2 步扫描那个空序列并且什么都不做
第 3 步插入到宏体 () 中,结果如下: ()
第 4 步扫描 () 查找宏,但未找到任何宏
所以第 2 步之后 X 的最终值为 TEST ()
第 3 步将其插入正文,给出 TEST ()
第 4 步抑制 EXPAND 并扫描第 3 步的结果以获取更多宏,发现 TEST 第 1 步得到参数的空序列 第 2 步不执行任何操作 第 3 步替换到正文中,给出 EXPAND(0) 第 4 步递归扩展它,抑制 TEST。此时,EXPAND 和 TEST 都被抑制(由于处于第 4 步扩展中),因此没有任何反应
第 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 步 TEST 被识别为具有空序列参数的宏
第 2 步 - 无(空标记序列中没有宏)
第 3 步,代入给出 EXPAND(0) 的正文
第 4 步,TEST 被抑制,结果递归扩展 第 1 步,EXPAND 被识别为宏(请记住,此时只有 TEST 被第 4 步递归抑制 - EXPAND 在第 2 步递归中,因此不被抑制)与 0作为其参数第 2 步,扫描 0 并且没有任何反应第 3 步,代入正文,给出 0 第 4 步,再次扫描 0 以查找宏(再次没有任何反应)
第 1 步,EXPAND 被识别为宏(请记住,此时只有 TEST 被第 4 步递归抑制 - EXPAND 在第 2 步递归中,因此不被抑制),其参数为 0
第 2 步,扫描 0 并且没有任何反应
步骤 3,代入给 0 的身体
第 4 步,再次扫描 0 以查找宏(再次没有任何反应)
第 3 步,将 0 作为参数 X 替换到第一个 EXPAND 的主体中
第 4 步,再次扫描 0 以查找宏(再次没有任何反应)
所以这里的最终结果是 0
对于这种情况,宏替换有三个相关步骤:
对参数执行宏替换。用它的定义替换宏,用参数替换参数。重新扫描结果以进行进一步替换,同时禁止替换的宏名称。
在 EXPAND(TEST PARENTHESIS())
中:
第一步,对EXPAND的参数进行宏替换,TEST PARENTHESIS():TEST后面没有括号,所以不解释为宏调用。 PARENTHESIS() 是一个宏调用,因此执行了三个步骤: 参数为空,因此不对其进行处理。然后 PARENTHESIS() 被 () 替换。然后 () 被重新扫描,没有找到宏。第 1 步完成,我们有了 EXPAND(TEST ())。 (TEST () 不会被重新扫描,因为它不是任何宏替换的结果。)
TEST 后面没有括号,因此它不会被解释为宏调用。
PARENTHESIS() 是一个宏调用,因此执行了三个步骤: 参数为空,因此不对其进行处理。然后 PARENTHESIS() 被 () 替换。然后 () 被重新扫描,没有找到宏。
第 1 步完成,我们有了 EXPAND(TEST ())。 (TEST () 不会被重新扫描,因为它不是任何宏替换的结果。)
第2步,将EXPAND(TEST ())替换为TEST()。
Step 3,TEST()在抑制EXPAND的同时被重新扫描: Step 1,参数是空的,所以没有对其进行处理。第 2 步,将 TEST () 替换为 EXPAND(0)。第 3 步,重新扫描 EXPAND(0),但 EXPAND 被抑制。
第 1 步,参数为空,因此不对其进行处理。
第 2 步,将 TEST () 替换为 EXPAND(0)。
第 3 步,重新扫描 EXPAND(0),但 EXPAND 被抑制。
在 EXPAND(TEST ())
中:
Step 1,对EXPAND的参数进行宏替换: Step 1,TEST的参数为空,不做任何处理。第 2 步,将 TEST () 替换为 EXPAND(0)。第三步,重新扫描这个替换,将EXPAND(0)替换为0。
第1步,TEST的参数是空的,所以没有处理。
第 2 步,将 TEST () 替换为 EXPAND(0)。
第三步,重新扫描这个替换,将EXPAND(0)替换为0。
第2步,EXPAND(TEST())变成了EXPAND(0),EXPAND(0)被0替换。
第 3 步,重新扫描 0 以获取更多宏,但没有。
问题中的其他示例也类似。它归结为:
在 TEST PARENTHESIS() 中,在 TEST 之后缺少括号会导致在处理封闭宏调用的参数时它不会被扩展。
PARENTHESIS 展开时,括号会放在它后面,但这是在扫描 TEST 之后,并且在处理参数期间不会重新扫描。
替换封闭宏后,将重新扫描 TEST 并随后替换,但此时封闭宏的名称被隐藏。
在阅读 Chris Dodd's masterful answer 并花一些时间思考之后,我想我已经解决了这个问题。
如果您像正常人一样使用 C 预处理器,那么这里确实没有问题要避免。只是不要制作相互提及的宏,你会没事的。但是,如果您涉足 the dark arts,您会发现很容易偶然发现上述问题。所以在这里,我将解释如何避免它。
请注意,我不会解释为什么会在这里发生(克里斯的回答已经很好地解释了原因),但我会列出它们发生的位置以及如何解决它们。
当您的宏“调用”具有延迟/间接扩展时,可能会出现问题。我所说的“间接”是一种无法立即进行的扩展,只能在一些连接或替换/替换之后进行。为了理解它,让我们先看一个安全的例子:
#define EXPAND(MACRO) MACRO
#define MY_MACRO(A, B) A##B
EXPAND(MY_MACRO(1,2)) // 12
在这里,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
#define EXPAND(MACRO) MACRO (1,2)
#define MY_MACRO(A, B) A##B
EXPAND(MY_MACRO) // 12
请注意,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.
#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.
另一方面,我们的示例没有间接扩展,是完全安全的,并且可以按预期进行评估:
#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.
由于 MY_MACRO(1,2)
是一个实际的宏,因此可以立即对其进行评估,并且不会出现任何问题。
如果我的判断是正确的,您不必担心任何以前的“被调用者”,只需担心直接以“延迟宏”作为参数的那个。您也不必担心其他参数,因为前身也会在替换之前尝试完全扩展它们。此外,如果不需要宏的完全扩展,这不会造成问题。
但是,如果您希望参数中的宏完全扩展并且它的扩展将是间接的,请仔细检查它。
#
和##
行为差异的解释。当我阅读 OP 的问题时,我一直期待这两个出现 - 我很惊讶他们没有出现!