c - 当宏间接扩展自身时,了解 C 的预处理器的行为

标签 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)
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 ,但出于某种原因,它绝对拒绝在最后一步再次扩展它。
现在,我将如何在我的实际程序中解决这个问题既不在这里也不在那里。尽管解决方案会很好(即一种将 token 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 步扫描该空序列并且不执行任何操作
  • step 3 插入宏体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 (嗯)
  • 第 1 步得到 AB对于参数(哦不)
  • 第 2 步对它们没有任何作用
  • 步骤 3 代入体给出 A B (嗯……)
  • 第 4 步扫描 A B用于宏,但什么也没找到

  • 最终结果是 A B

  • 这将是我们的梦想。可悲的是,宏扩展为 GLUE(A,B) .
    所以我们的问题是:为什么?

    最佳答案

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

  • 当一个带参数的宏被识别(宏名称标记后跟 ( 标记)时,以下标记直到匹配的 )被扫描和拆分(在 , token 上)。发生这种情况时不会发生宏扩展(因此 , s 和 ) 必须直接出现在输入流中,不能出现在其他宏中)。
  • 名称出现在宏体中的每个宏参数前面没有 ###或后跟 ##是“预扫描”的,以便宏展开——完全在参数中的任何宏都将在替换到宏体之前递归展开。
  • 生成的宏参数标记流被替换到宏的主体中。 #涉及的论据或 ##操作被修改(字符串化或粘贴)并基于来自步骤 1 的原始解析器标记进行替换(步骤 2 不会发生这些)。
  • 再次扫描生成的宏主体 token 流以查找要扩展的宏,但忽略当前正在扩展的宏。此时,输入中的更多标记(在步骤 1 中扫描和解析的内容之后)可以作为任何已识别宏的一部分包含在内。

  • 重要的是发生了两种不同的递归扩展(上面的第 2 步和第 4 步),并且只有第 4 步中的一个会忽略同一宏的递归宏扩展。步骤 2 中的递归扩展不会忽略当前宏,因此可以递归扩展它。
    因此,对于上面的示例,让我们看看会发生什么。对于输入
    EXPAND(TEST PARENTHESIS())
    
  • 第1步看到宏EXPAND并在参数列表中扫描 TEST PARENTHESIS()X
  • 第 2 步不识别 TEST作为宏(不跟随 ( ),但确实识别 PARENTHESIS :
  • 第 1 步(嵌套)获取参数
  • 的空标记序列
  • 第 2 步扫描该空序列并且不执行任何操作
  • step 3 插入宏体()仅产生:()
  • 第 4 步扫描 ()对于宏,没有找到任何

  • 所以 X 的最终值第 2 步之后是 TEST ()
  • 第 3 步将其插入主体,给出 TEST ()
  • 第 4 步抑制 EXPAND并扫描步骤 3 的结果以获取更多宏,找到 TEST
  • 第 1 步获取参数
  • 的空序列
  • 第 2 步什么都不做
  • 步骤 3 代入体给出 EXPAND(0)
  • 步骤 4 递归扩展,抑制 TEST .此时,EXPANDTEST被抑制(由于在第 4 步扩展中),所以没有任何 react


  • 你的另一个例子 EXPAND(TEST())是不同的
  • 第 1 步 EXPAND被识别为宏,并且 TEST()被解析为参数 X
  • 第 2 步,这个流被递归解析。请注意,由于这是第 2 步,EXPAND不被抑制
  • 第 1 步 TEST被识别为带有空序列参数的宏
  • 第 2 步——什么都没有(空 token 序列中没有宏)
  • 第 3 步,代入给 EXPAND(0) 的主体
  • 第 4 步,TEST被抑制并递归扩展结果
  • 第 1 步,EXPAND被识别为宏(请记住,此时只有 TEST 被第 4 步递归抑制——EXPAND 在第 2 步递归中因此不被抑制)与 0作为其参数
  • 第 2 步,0被扫描并且没有任何 react
  • 第3步,代入给0的正文
  • 第 4 步,0再次扫描宏(再次没有任何 react )


  • 第 3 步,0被替换为参数 X入体第一EXPAND
  • 第 4 步,0再次扫描宏(再次没有任何 react )

  • 所以这里的最终结果是 0

    关于c - 当宏间接扩展自身时,了解 C 的预处理器的行为,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/66593868/

    相关文章:

    c - 将 C 编写为 s 表达式

    Java 替代 C 宏

    c - 语法错误: missing ')' before 'constant'

    c++ - 在 C 和 C++ 中,为什么每个 .h 文件通常都用#ifndef#define #endif 指令包围?

    谁能帮我理解这段代码有什么问题?

    c - 为什么这个简单的C命令行工具总是退出而不工作?

    visual-c++ - %(AdditionalIncludeDirectories) 含义

    C 预处理器:计算结构数组中的元素数

    无法将缓冲区的内容复制到 C 中的字符串 : reading from . bin 文件中

    C 拼图 {MACRO}