【问题标题】:C/C++ macro nested replacement logicC/C++ 宏嵌套替换逻辑
【发布时间】:2021-08-23 03:30:53
【问题描述】:

我正在尝试实现与 C/C++ 兼容的宏处理。我可以正确处理许多极端情况,包括此处讨论的情况:Understanding the behavior of C's preprocessor when a macro indirectly expands itself

但是,有一个极端情况,我从 gcc 和 clang 得到不同的答案,所以显然是错误的。代码类似于这里讨论的案例:Can we have recursive macros?

#define ID(arg) arg
#define EMPTY
#define NOEXPAND(macro) macro EMPTY
#define F_AGAIN() F
#define F() f NOEXPAND(F_AGAIN)()()

ID(F())

我得到f F (),但 gcc/clang 输出 f f F_AGAIN ()()。我的逻辑是f NOEXPAND(F_AGAIN)()() 中的每个标记在其“隐藏集”中都有F,并且您永远无法从标记的隐藏集中删除元素。

Can we have recursive macros? 中,最相关的答案是关于将宏绘制为蓝色而不是隐藏集,但我找不到任何关于其工作原理的完整描述。因此,我遵循Dave Prosser's 算法,该算法根据隐藏集解释了 cpp 行为。我认为这就是编译器在实践中所做的。

我的问题:我是在误解 Prosser 的算法,还是应该实施不同的算法?现代 C 预处理器的行为是否记录在任何地方?语言规范本身在这个问题上过于含糊。

【问题讨论】:

  • 我将不得不更详细地查看这一点,但考虑到在处理 ID(F()) 时,首先替换 ID 参数中的宏。所以F() 被处理了。一旦处理完成,它就结束了,完成,完成。以后的压制不记得了。这就像一个递归函数被调用并返回。它不再嵌套在任何东西中。然后ID(f F_AGAIN ()()) 被重新处理。
  • @πάνταῥεῖ:预处理器替换做“简单、直接的文本替换”。首先,它对预处理器标记进行操作,而不是文本。其次,它有关于其操作顺序的规则,以及何时考虑替换令牌以及不考虑进一步替换。第三,它有运算符(###)。我看到你错误地关闭了这个用户的上一个问题,因为它是重复的。请不要那样做。
  • @πάνταῥεῖ 众所周知,MSVC 多年来一直有一个错误的预处理器,所以它并不是那么简单。我所说的“窃听”是指像所讨论的那样的次要细节,但它破坏了复杂的宏。
  • @HolyBlackCat 这正是我开始的地方,但事实证明该标准在这一点上故意存在缺陷,因为他们担心影响现有编译器的合规性状态。如果标准指定了行为,我可能不会在这里询问它。不相信我?请参阅:open-std.org/jtc1/sc22/wg21/docs/cwg_active.html#268(但请注意,我在该讨论中处理此案,这似乎与我对 Prosser 的阅读一致)。
  • “委员会的决定是“野外”的现实项目不会冒险进入这个领域”哈!

标签: c++ c c-preprocessor


【解决方案1】:

好的,我想我已经弄清楚发生了什么。编译器不实现 Prosser 的算法,时期。尽管 Prosser 的算法确实解析了语言规范中的特定 known defect,但它并没有解释实际编译器扩展宏的方式。

似乎有两种禁用位在起作用,而不是隐藏集:每个宏禁用位和每个令牌禁用位。

每当M 被扩展时,宏M 的每个宏禁用位都会在重新扫描阶段设置,并在M 的扩展完成后被清除。

如果在禁用M 的任何时候,预处理器处理M 的实例(无论在语法上是否可用于扩展),它会将特定的M 标记标记为禁用。编译器不仅在禁用令牌时避免扩展令牌,而且在任何上下文中都永久禁用对扩展令牌的任何考虑,即使在M 的扩展完成之后也是如此。

让我们看一些更简单的例子:

#define ID(arg) arg
#define LPAREN (

#define F_AGAIN() F
#define F() f F_AGAIN LPAREN)()

F()         // => f F_AGAIN ()()
ID(F())     // => f f F_AGAIN ()()
ID(ID(F())) // => f f f F_AGAIN ()()

#define G() g G LPAREN)()

G()         // => g G ()()
ID(G())     // => g G ()()
ID(ID(G())) // => g G ()()

首先考虑在G() 的扩展中发生了什么。它扩展到令牌列表g G LPAREN)(),并且在重新扫描期间,该令牌列表中的G 被永久禁用。现在无论您通过ID 传递令牌列表多少次重新扫描令牌列表,G 永远无法扩展。

接下来考虑F() 发生了什么。它扩展到令牌列表f F_AGAIN LPAREN)()。在重新扫描期间,这将扩展为 f F_AGAIN ()()。因为F_AGAIN 当前不是禁用的宏,所以这些输出标记都不会被禁用。所以现在在对ID 宏的任何重新扫描中,F_AGAIN 将被展开一次,同时导致F 被展开一次。

在这种情况下,实际上可以理解language spec

如果在替换列表的扫描过程中找到被替换的宏的名称(不包括源文件的其余预处理标记),则不会替换它。 此外,如果任何嵌套替换遇到被替换的宏的名称,它不会被替换。 这些未替换的宏名称预处理标记不再可用于进一步替换,即使它们稍后在本应替换宏名称预处理标记的上下文中(重新)检查。

我猜想与我的直觉混淆的部分是“它没有被替换”听起来如此无害 - 特别是在宏不会被替换的情况下,例如因为它是一个类似函数的宏,后面没有打开括号(。然后“令牌不再可用于进一步替换”中的被动语态听起来像是在描述前一句的结果,而规范的真正意思是,“编译器主动毒化该令牌并禁止它再次被扩展。”

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2019-06-06
    • 2013-03-13
    • 1970-01-01
    • 1970-01-01
    • 2016-08-08
    • 1970-01-01
    相关资源
    最近更新 更多