ChatGPT解决这个技术问题 Extra ChatGPT

了解当宏间接扩展自身时 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)”在第一种情况下是正确的扩展,而在其他情况下却不是?

虽然很容易在 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)

所以我们的问题是:为什么?

我喜欢这个问题(以及你是如何问的)。我不会在这里提交答案,但我对“递归”函数式宏解析的想法非常怀疑。您期望两个宏生成看起来像另一个类似函数的宏的文本然后期望它本身被评估的事情似乎......一个太多了
这可能属于 §6.10.3.4/p4 的规定,“在某些情况下,不清楚替换是否嵌套。”然后以“不允许严格遵守的程序依赖于这种未指定的行为”作为结尾。
@user3386109 确实 6.10.3.4 描绘了一幅相当不错的画面:"[...] 重新扫描生成的预处理标记序列 [...],以替换更多宏名称。如果宏的名称在替换列表的扫描过程中发现被替换的 [...],它不会被替换。此外,如果任何嵌套替换遇到被替换的宏的名称,它不会被替换 b>。”...
...“这些未替换的宏名称预处理标记不再可用于进一步替换,即使它们稍后(重新)在宏名称预处理标记将被替换的上下文中进行检查。”
将数百行代码转换为由六行或更少行组成的 MWE 应该并不少见。

C
Chris Dodd

宏扩展是一个复杂的过程,只有通过了解发生的步骤才能真正理解。

当一个带有参数的宏被识别时(宏名记号后跟(记号),后面的记号直到匹配的记号)被扫描和分割(上记号,记号)。发生这种情况时不会发生宏扩展(因此 ,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


很好,特别是对###行为差异的解释。当我阅读 OP 的问题时,我一直期待这两个出现 - 我很惊讶他们没有出现!
很好的答案!我试图对其进行“压力测试”,并提出了一个反例,我无法用分步解决方案来解释。我在对我的问题进行编辑时将其添加为新挑战者。
@LuizMartins:我很确定答案归结为6.10.3.4 paragraph 4:“在某些情况下,不清楚替换是否嵌套......严格符合的程序不允许依赖于这种未指定的行为。”基本上,宏替换语义未指定。这是一个已知问题,试图确定确切的语义是徒劳的。
@user2357112supportsMonica 实际上,上述行为似乎是“标准本身未指定”。非常感谢您的链接。
@ChrisDodd 如果您希望可以将链接添加到 6.10.3.4,并且我添加的示例与第 4 段中的示例非常相似,这是标准未定义的行为,我会接受您的回答。
E
Eric Postpischil

对于这种情况,宏替换有三个相关步骤:

对参数执行宏替换。用它的定义替换宏,用参数替换参数。重新扫描结果以进行进一步替换,同时禁止替换的宏名称。

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 并随后替换,但此时封闭宏的名称被隐藏。


L
Luiz Martins

在阅读 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) 是一个实际的宏,因此可以立即对其进行评估,并且不会出现任何问题。

如果我的判断是正确的,您不必担心任何以前的“被调用者”,只需担心直接以“延迟宏”作为参数的那个。您也不必担心其他参数,因为前身也会在替换之前尝试完全扩展它们。此外,如果不需要宏的完全扩展,这不会造成问题。

但是,如果您希望参数中的宏完全扩展并且它的扩展将是间接的,请仔细检查它。


一个小小的术语唠叨:宏被扩展但从未被调用!你很容易引起混淆,特别是在包含函数的程序的上下文中——这些函数确实被调用了。
一种简单的表达方式可能是“C预处理器完全扩展每个宏定义一次,然后不能第二次扩展动态定义(通过其名称的扩展结果和其参数的扩展结果动态构建宏)” ,你不觉得吗?
@Zilog80 我不太对,因为预处理器完全能够多次扩展宏定义,即使在嵌套情况下,但 当且仅当该扩展直接发生在参数中(即in the 2nd step of expansion, instead of the 4th one)。
@LuizMartins 我同意表达这种方式可能会产生误导。即使使用“全局一次”,因为它会误导嵌套扩展。我会给它更多的反思。

关注公众号,不定期副业成功案例分享
关注公众号

不定期副业成功案例分享

领先一步获取最新的外包任务吗?

立即订阅