令牌连接运算符 ## 的应用如何与禁止递归宏扩展交互?

How does application of the token concatenation operator ## interact with the prohibition against recursive macro expansion?

提问人:Lover of Structure 提问时间:10/10/2023 最后编辑:Lover of Structure 更新时间:10/10/2023 访问量:67

问:

标准和问题的规则

该标准对宏扩展的重新扫描阶段(在 /-处理和参数替换之后)进行了如下说明(C17 草案,6.10.3.4 ¶2):###

如果在扫描替换列表期间找到要替换的宏的名称(不包括源文件的其余预处理标记),则不会替换该宏。此外,如果任何嵌套替换遇到要替换的宏的名称,则不会替换该宏。这些未替换的宏名称预处理标记不再可用于进一步替换,即使稍后在该宏名称预处理令牌将被替换的上下文中对其进行(重新)检查也是如此。

让我借此机会总结一下宏替换如何与类似对象或函数的宏 M 的 /-processing 和参数替换进行交互:###

  1. 后面或相邻的参数将被逐字替换(可能作为地标标记),然后由 和 处理。######
  2. 对于每个其他参数,首先将其对应的参数完全宏展开,然后由结果替换该参数。
    • 这些参数的宏扩展就好像它们孤立存在一样,即不考虑替换列表中的参数之后括号中的函数参数,这与作为参数参数的一部分提供的函数参数不同。
  3. 将删除所有由此产生的地标标记。##
  4. 将重新扫描生成的标记序列 S 以及源文件的所有后续预处理标记,以便评估更多宏。
    • 在此过程中,即使在另一个上下文中检查,也不会在 S 中进一步出现 M 或 S 的扩展。

(此算法描述与标准中的描述并不完全相同,但至少就本文而言,它应该足够等效。

现在的问题是:令牌连接运算符 ## 的应用如何与禁止递归宏扩展相互作用?具体来说,它如何影响某些递归扩展被阻塞的区域的边界?GCC 和 MSVC 似乎对带有地标标记的串联与普通串联的处理方式不同。

让我们考虑以下示例:

#define RECURSIONTEST(a, b, c)  a ## c + b ## c
#define AC  A
#define A  AC A
#define CALL_RC(x, y, z)  RECURSIONTEST(x, y, z)

CALL_RC(AC, A, C)
CALL_RC(AC, A, )

(预处理器可以按如下方式运行:或 (GCC)、(MSVC)。cppgcc -Ecl /E

这两个宏的计算结果如下

CALL_RC(AC, A, C)  ->  AC AC A + A AC A
CALL_RC(AC, A, )   ->  AC A + A A

同时使用 GCC 和 MSVC。

让我们手动计算应该发生什么:CALL_RC(AC, A, C)

CALL_RC(AC, A, C)
  [def of CALL_RC(x, y, z):  RECURSIONTEST(x, y, z)]
  arg x:  AC  ->  A /*blk:AC*/  ->  AC A /*blk:AC,A*/
  arg y:  A  ->  AC A /*blk:A*/  ->  A A /*blk:A,AC*/
  arg z:  C
  after arg subst:
    RECURSIONTEST(AC A /*blk:AC,A*/, A A /*blk:A,AC*/, C)
    // rescan for add'l macros:
RECURSIONTEST(AC A /*blk:AC,A*/, A A /*blk:A,AC*/, C) /*blk: CALL_RC*/
  [def of RECURSIONTEST(a, b, c):  a ## c + b ## c]
  ##-arg a:  AC A /*blk:AC,A*/
  ##-arg b:  A A /*blk:A,AC*/
  ##-arg c:  C
  concat 1:  AC AC
    // blocked for the left-hand AC:  AC, A
    // Are A and AC blocked for the right-hand AC?
  concat 2:  A AC
    // blocked for A:  A, AC
    // Are A and AC blocked only for A or also for AC?
  after ##:
    AC AC + A AC
    // rescan for add'l macros:
AC AC + A AC
  // blocked globally:  CALL_RC, RECURSIONTEST
  // blocked for the 1st AC:  AC, A
  // perhaps (*) blocked locally for the 2nd and 3rd AC:  A, AC
  // blocked for A:  A, AC
  after macro expansion:
    case 1 (blocking for (*)):
      result:  AC AC + A AC /*blk:AC,A*/
    case 2 (no blocking for (*)):
      2nd/3rd AC:  AC  ->  A /*blk:AC*/  ->  AC A /*blk:AC,A*/
      result:  AC AC A + A AC A /*blk:A,AC*/
  // no further expansion possible in either case

在这里,“blk”表示前面表达式中的“为递归扩展而阻止的宏”。特别指出了局部块(用于子表达式)。

现在让我们对以下方面进行相同的计算:CALL_RC(AC, A, )

CALL_RC(AC, A, )
  [def of CALL_RC(x, y, z):  RECURSIONTEST(x, y, z)]
  arg x:  AC  ->  A /*blk:AC*/  ->  AC A /*blk:AC,A*/
  arg y:  A  ->  AC A /*blk:A*/  ->  A A /*blk:A,AC*/
  arg z:  <empty>
  after arg subst:
    RECURSIONTEST(AC A /*blk:AC,A*/, A A /*blk:A,AC*/, )
    // rescan for add'l macros:
RECURSIONTEST(AC A /*blk:AC,A*/, A A /*blk:A,AC*/, ) /*blk: CALL_RC*/
  [def of RECURSIONTEST(a, b, c):  a ## c + b ## c]
  ##-arg a:  AC A /*blk:AC,A*/
  ##-arg b:  A A /*blk:A,AC*/
  ##-arg c:  <placemarker>
  concat 1:  AC A
    // blocked for AC:  AC, A
    // Are A and AC blocked for A?
  concat 2:  A A
    // blocked for the left-hand A:  A, AC
    // Are A and AC blocked for the right-hand A?
  after ##:
    AC A + A A
    // rescan for add'l macros:
AC A + A A
  // blocked globally:  CALL_RC, RECURSIONTEST
  // blocked for AC:  AC, A
  // perhaps (**) blocked locally for the 1st and 3rd A:  A, AC
  // blocked for the 2nd A:  A, AC
  after macro expansion:
    case 1 (blocking for (**)):
      result:  AC A + A A /*blk:AC,A*/
    case 2 (no blocking for (**)):
      1st/3rd A:  A  ->  AC A /*blk:A*/  ->  A A /*blk:A,AC*/
      result:  AC A A + A A A /*blk:A,AC*/
  // no further expansion possible in either case

分析

CALL_RC(AC, A, C)
GCC 和 MSVC () 的输出对应于情况 2(“无阻塞”)。具体而言,令牌串联产生的两个实例的阻止列表将重置,从而允许并重新展开。
AC AC A + A AC AACACA

CALL_RC(AC, A, )
GCC 和 MSVC () 的输出对应于情况 1(“阻塞”)。具体而言,由令牌与地标连接生成的两个实例(即与空参数生成的令牌相邻)不会重置其阻止列表,从而阻止进一步扩展。
AC A + A AA##

诚然,对这两种情况进行区别对待是有道理的,但这实际上是从标准中得出的,还是标准对此含糊不清?我特别指的是 C17 草案的 6.10.3.4 ¶2 中的措辞(在本文的最顶部引用)。

可以说,答案取决于令牌在与地标连接后是否仍然是“相同”的令牌(C17 草案,6.10.3.3 ¶3)。


这个网站上有一个类似的问题:通过令牌串联重复的宏调用是否是未指定的行为?
注意:

  • 它的例子令人费解。
  • 它的答案没有详细说明。
  • 我的问题试图显示详细的分步评估。
  • 我的问题显示了两个宏观扩展,它们应该(可以想象)被同等对待,但事实并非如此。
language-lawyer c-preprocessor token-pasting-operator

评论

0赞 Jonathan Leffler 10/31/2023
我很好奇——这个“问题”中有多少是真正的“答案”?事实上,你要寻找答案的问题是什么?这应该成为一个带有自我答案的缩小问题吗?
2赞 pmor 11/16/2023
请注意:默认情况下,MSVC 预处理器不符合 C 标准。有一个选项 /Zc:preprocessor,它“启用符合 C99 和 C++ 及更高版本标准的基于令牌的预处理器”。请考虑使用 .另见 12/Zc:preprocessor
0赞 Lover of Structure 11/16/2023
@pmor 非常感谢你指出这一点。/ 对于任何看到这个的人:我刚刚用 测试了我的代码,谢天谢地,输出保持不变。/Zc:preprocessor

答: 暂无答案