【问题标题】:Understanding the behavior of C's preprocessor when a macro indirectly expands itself了解当宏间接扩展自身时 C 的预处理器的行为
【发布时间】: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 将扩展 MACROMACRO 将扩展 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。然而,它被抑制了,所以它就这样离开了。
  • 所以X在第2步之后的最终值为GLUE(A,B)(请注意,由于我们不在GLUE的第4步,理论上它不再被抑制了)
  • 第 3 步将其插入正文,给出GLUE(A,B)
  • 第 4 步抑制 EXPAND 并扫描 GLUE(A,B) 以获取更多宏,找到 GLUE (uuh)
    • 第 1 步获取 AB 作为参数(哦,不
    • 第 2 步对它们没有任何作用
    • 第 3 步替换成身体,给出A B嗯...
    • 第 4 步扫描 A B 中的宏,但什么也没找到
  • 那么最后的结果就是A B

这将是我们的梦想。遗憾的是,宏扩展为GLUE(A,B)

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

【问题讨论】:

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

标签: c macros c-preprocessor


【解决方案1】:

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

  1. 当一个带有参数的宏被识别时(宏名称标记后跟( 标记),以下标记直到匹配的) 被扫描和分割(在, 标记上)。发生这种情况时不会发生宏扩展(因此,s 和) 必须直接存在于输入流中,不能存在于其他宏中)。

  2. 名称出现在宏主体中的每个宏参数不是前面有### 或后面有## 的宏被“预扫描”以供宏展开——任何完全在其中的宏在代入宏体之前,该参数将被递归扩展。

  3. 生成的宏参数标记流被替换到宏的主体中。 ### 操作中涉及的参数会根据步骤 1 中的原始解析器标记进行修改(字符串化或粘贴)和替换(这些参数不会发生步骤 2)。

  4. 再次扫描生成的宏主体令牌流以查找要扩展的宏,但忽略当前正在扩展的宏。此时,输入中的其他标记(在步骤 1 中扫描和解析的内容之后)可以作为识别的任何宏的一部分包含在内。

重要的是发生了两种不同的递归扩展(上面的第 2 步和第 4 步),并且只有第 4 步中的一个会忽略同一宏的递归宏扩展。步骤 2 中的递归扩展不会忽略当前宏,因此可以递归扩展。

因此,对于上面的示例,让我们看看会发生什么。对于输入

EXPAND(TEST PARENTHESIS())
  • 步骤 1 看到宏 EXPAND 并在参数列表 TEST PARENTHESIS() 中扫描 X
  • 第 2 步不能将 TEST 识别为宏(没有跟随 (),但可以识别 PARENTHESIS
    • 第 1 步(嵌套)获取参数的空标记序列
    • 第 2 步扫描该空序列并且不执行任何操作
    • 第 3 步插入到宏体 () 中,结果是:()
    • 第 4 步扫描 () 以查找宏,但未找到任何宏
  • 所以第二步之后X 的最终值为TEST ()
  • 第 3 步将其插入正文中,给出TEST ()
  • 第 4 步抑制 EXPAND 并扫描第 3 步的结果以查找更多宏,找到 TEST
    • 第 1 步获取参数的空序列
    • 第 2 步不执行任何操作
    • 第 3 步替换成身体给EXPAND(0)
    • 第 4 步递归扩展了它,抑制了TEST。此时,EXPANDTEST 都被抑制(由于处于第 4 步扩展中),因此没有任何反应

你的另一个例子 EXPAND(TEST()) 是不同的

  • 第1步EXPAND被识别为宏,TEST()被解析为参数X
  • 第 2 步,递归解析此流。请注意,由于这是第 2 步,EXPAND 不会被禁止
    • 步骤 1 TEST 被识别为具有空序列参数的宏
    • 第 2 步 - 无(空标记序列中没有宏)
    • 第三步,代入EXPAND(0)的正文
    • 第四步,TEST被抑制,结果递归展开
      • 第 1 步,EXPAND 被识别为宏(请记住,此时只有 TEST 被第 4 步递归抑制 -- EXPAND 在第 2 步递归中,因此不会被抑制)与0作为它的论据
      • 第 2 步,0 被扫描,但没有任何反应
      • 第三步,代入正文给0
      • 第 4 步,0 再次扫描宏(再次没有任何反应)
  • 第三步,0 被替换为参数X 到第一个EXPAND 的正文中
  • 第 4 步,0 再次扫描宏(再次没有任何反应)

所以这里的最终结果是0

【讨论】:

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

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

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

EXPAND(TEST PARENTHESIS()):

  • 第一步,对EXPANDTEST 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 ()):

  • 第一步,对EXPAND的参数进行宏替换:
    • 第一步,TEST的参数为空,所以没有处理。
    • 第2步,TEST ()EXPAND(0)替换。
    • 第三步,重新扫描这个替换,将EXPAND(0)替换为0
  • 第2步,EXPAND(TEST ())变成EXPAND(0)EXPAND(0)0替换。
  • 第 3 步,0 被重新扫描以获取更多宏,但没有。

问题中的其他示例类似。归结为:

  • TEST PARENTHESIS() 中,TEST 后面缺少括号会导致它在处理封闭宏调用的参数时不会被扩展。
  • PARENTHESIS 展开时,括号会放在后面,但这是在扫描 TEST 之后,在处理参数期间不会重新扫描。
  • 封闭宏被替换后,TEST 被重新扫描并被替换,但此时封闭宏的名称被隐藏。

【讨论】:

    【解决方案3】:

    在阅读Chris Dodd's masterful answer 并花一些时间思考之后,我想我已经解决了这个问题。

    如果您像一个正常而理智的人一样使用 C 预处理器,那么这里确实没有要避免的问题。只是不要制作相互提及的宏,你会没事的。但是,如果您涉足the dark arts,您会发现很容易偶然发现上述问题。所以在这里,我将解释如何避免它。

    请注意,我不会解释 为什么 这些会发生在这里(Chris 的回答已经很好地解释了为什么 非常),但我会列出它们发生在哪里以及如何解决它们。


    当您的宏“调用”具有延迟/间接扩展时,可能会出现问题。我所说的“间接”是一种无法立即进行的扩展,只能在一些连接或替换/替换之后进行。为了理解它,我们先看一个安全的例子:

    #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 我同意表达这种方式可能会产生误导。即使使用“全局一次”,因为它会误导嵌套扩展。我会给它更多的思考。
    猜你喜欢
    • 2020-10-18
    • 1970-01-01
    • 2018-01-04
    • 1970-01-01
    • 2020-09-18
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多