在 ## 运算符存在的情况下,可变参数 GNU C 预处理器宏的惊人扩展

Surprising expansion of variadic GNU C preprocessor macros in the presence of the ## operator

如果我们定义一个宏

#define M(x, ...) { x, __VA_ARGS__ }

然后使用它作为参数传递自身

M(M(1, 2), M(3, 4), M(5, 6))

然后扩展为预期的形式:

{ { 1, 2 }, { 3, 4 }, { 5, 6 } }

然而,当我们使用 ## 运算符时(为了防止在单参数调用的情况下在输出中出现悬挂逗号,如记录 in the GCC manual),即

#define M0(x, ...) { x, ## __VA_ARGS__ }

然后

中参数的扩展
M0(M0(1,2), M0(3,4), M0(5,6))

似乎在第一个参数之后停止,即我们得到:

{ { 1,2 }, M0(3,4), M0(5,6) }

这种行为是错误,还是源于某种原则?

(我也用clang查过,和GCC一样)

在这个答案的最后有一个可能的解决方案。

Is this behavior a bug, or does it stem from some principle?

它源于两个相互作用非常微妙的原则。所以我同意这很令人惊讶,但这不是错误。

两个原则如下:

  1. 宏调用替换里面,那个宏没有展开。 (参见 GCC Manual Section 3.10.5, Self-Referential Macros or the C Standard, §6.10.3.4 paragraph 2.) This precludes recursive macro expansion, which in most cases would produce infinite recursion if allowed. Although it is likely that no-one anticipated such uses, it turns out that there would be ways of using recursive macro expansion which would not result in infinite recursion (see the Boost Preprocessor Library documentation for a thorough discussion of this issue),但标准现在不会改变。

  2. 如果将 ## 应用于宏参数,它会抑制该参数的宏扩展。 (参见 GCC Manual section 3.5, Concatenation or the C Standard, §6.10.3.3 paragraph 2.) The suppression of expansion is part of the C Standard, but GCC/Clang's extension to allow use of ## to conditionally suppress the comma preceding __VA_ARGS__ is non-standard. (See the GCC Manual Section 3.6, Variadic Macros。)显然,该扩展仍然遵循标准关于不扩展串联宏参数的规则。

关于可选的逗号抑制,关于第二点的奇怪之处在于您在实践中几乎没有注意到它。您可以使用 ## 有条件地取消逗号,并且参数仍会正常扩展:

#define SHOW_ARGS(arg1, ...) Arguments are (arg1, ##__VA_ARGS__)
#define DOUBLE(a) (2 * a)
SHOW_ARGS(DOUBLE(2))
SHOW_ARGS(DOUBLE(2), DOUBLE(3))

扩展为:

Arguments are ((2 * 2))
Arguments are ((2 * 2), (2 * 3))

DOUBLE(2)DOUBLE(3) 都正常扩展,尽管其中之一是连接运算符的参数。

但是宏扩展有一个微妙之处。扩展发生两次:

  1. 首先,扩展宏参数。 (此扩展位于调用宏的文本的上下文中。)这些扩展参数替换宏替换主体中的参数(但仅在参数不是 # 或 [=19= 的参数的情况下) ]).

  2. 然后将 ### 运算符应用于替换标记列表。

  3. 最后,将生成的替换标记插入到输入流中,以便再次展开。这一次,扩展是在宏的上下文中进行的,因此递归调用被抑制了。

考虑到这一点,我们看到在 SHOW_ARGS(DOUBLE(2), DOUBLE(3)) 中,DOUBLE(2) 在步骤 1 中展开,然后插入替换标记列表,DOUBLE(3) 在步骤 1 中展开3,作为替换令牌列表的一部分。

这与 SHOW_ARGS 中的 DOUBLE 没有区别,因为它们是不同的宏。但是如果它们是同一个宏,区别就会很明显。

要查看差异,请考虑以下宏:

#define INVOKE(A, ...) A(__VA_ARGS__)

该宏创建一个宏调用(或函数调用,但这里我们只对它是一个宏的情况感兴趣)。即依次INVOKE(X, Y)变为X(Y)。 (这是一个有用功能的简化,其中命名的宏实际上被调用了多次,可能参数略有不同。)

SHOW_ARGS 配合使用效果很好:

INVOKE(SHOW_ARGS, one arg)

⇒ Arguments are (one arg)

但是如果我们尝试INVOKEINVOKE本身,我们发现禁止递归调用生效:

INVOKE(INVOKE, SHOW_ARGS, one arg)

⇒ INVOKE(SHOW_ARGS, one arg)

"Of course",我们可以将 INVOKE 作为参数扩展到 INVOKE:

INVOKE(SHOW_ARGS, INVOKE(SHOW_ARGS, one arg))

⇒ Arguments are (Arguments are (one arg))

这很好用,因为 INVOKE 中没有 ##,所以不会抑制参数的扩展。但是如果参数的展开被抑制了,那么参数就会原封不动地插入到宏体中,然后就变成了递归展开。

这就是您的示例中发生的事情:

#define M0(x, ...) { x, ## __VA_ARGS__ }
M0(M0(1,2), M0(3,4), M0(5,6))

⇒ { { 1,2 }, M0(3,4), M0(5,6) }

这里,外层M0M0(1,2)的第一个参数没有和##一起使用,所以作为调用的一部分展开。其他两个参数是 __VA_ARGS__ 的一部分,与 ## 一起使用。因此,它们在被替换到宏的替换列表之前不会展开。但是作为宏替换列表的一部分,它们的扩展被非递归宏规则抑制了。

你可以通过定义两个版本的 M0 宏来轻松解决这个问题,它们具有相同的内容但不同的名称(如对 OP 的评论中所建议的):

#define M0(x, ...) { x, ## __VA_ARGS__ }
M0(M1(1,2), M1(3,4), M1(5,6))

⇒ { { 1,2 }, { 3,4 }, { 5,6 } }

但这不是很愉快。

解决方法:使用__VA_OPT__

C++2a 将包括一个新功能,专门用于帮助在可变参数调用中抑制逗号:__VA_OPT__ 类函数宏。在可变参数宏扩展中,__VA_OPT__(x) 扩展到它的参数,前提是可变参数中至少有一个标记。但是,如果 __VA_ARGS__ 扩展为一个空的标记列表,那么 __VA_OPT__(x) 也是如此。因此,__VA_OPT__(,) 可以像 GCC ## 扩展一样用于逗号的条件抑制,但与 ## 不同的是,它不会触发宏扩展的抑制。

作为 C 标准的扩展,最新版本的 GCC 和 Clang 为 C 和 C++ 实现了 __VA_OPT__。 (参见 GCC Manual Section 3.6, Variadic Macros。)因此,如果您愿意依赖相对较新的编译器版本,则有一个非常干净的解决方案:

#define M0(x, ...) { x __VA_OPT__(,) __VA_ARGS__ }
M0(M0(1,2), M0(3,4), M0(5,6))

⇒ { { 1 , 2 } , { 3 , 4 }, { 5 , 6 } }

备注:

  1. 您可以在 Godbolt

  2. 上查看这些示例
  3. 这个问题最初是作为 Variadic macros: expansion of pasted tokens 的重复问题而关闭的,但我认为这个答案对于这种特殊情况来说还不够。