首页 > 解决方案 > 了解当宏间接扩展自身时 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 线:

#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 的图灵完备性。可悲的是,对于那里的预处理器向导来说,上面的宏不扩展是一种保证。

失败并不是真正的问题,真正的问题是:在哪里?预处理器在哪里决定停止扩展?

分析步骤:

这将是我们的梦想。可悲的是,宏扩展为GLUE(A,B).

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

标签: cmacrosc-preprocessor

解决方案


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

  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所以第 2 步之后的最终值是TEST ()
  • 第 3 步将其插入体内,给出TEST ()
  • 第 4 步抑制EXPAND并扫描第 3 步的结果以获取更多宏,发现TEST
    • 第 1 步获取参数的空序列
    • 第 2 步什么都不做
    • step 3 代入身体EXPAND(0)
    • 第 4 步递归扩展了它,抑制TEST. 此时,EXPANDTEST都被抑制(由于处于第 4 步扩展中),因此没有任何反应

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

  • step 1EXPAND被识别为宏,并被TEST()解析为参数X
  • 第 2 步,递归解析该流。请注意,由于这是第 2 步,EXPAND因此不被禁止
    • 步骤 1TEST被识别为具有空序列参数的宏
    • 第 2 步 - 无(空标记序列中没有宏)
    • step 3,代入body给EXPAND(0)
    • 第四步,TEST被抑制,结果递归展开
      • 第 1 步,EXPAND被识别为宏(请记住,此时仅TEST被第 4 步递归抑制 -EXPAND在第 2 步递归中,因此不被抑制)0作为其参数
      • 第 2 步,0被扫描,没有任何反应
      • 第三步,代入体内0
      • 第 4 步,0再次扫描宏(再次没有任何反应)
  • 第 3 步,将 the0作为参数替换X到第一个的主体中EXPAND
  • 第 4 步,0再次扫描宏(再次没有任何反应)

所以这里的最终结果是0


推荐阅读