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



如果我们定义宏

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

然后将其作为自变量传递

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

然后它扩展到预期的形式:

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

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

#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相同)

在这个答案的末尾有一个可能的解决方案。

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

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

两个原则如下:

  1. 在宏调用的替换中,该宏不会展开。(参见GCC手册第3.10.5节,自参考宏或C标准,§6.10.3.4第2段。)这排除了递归宏扩展,在大多数情况下,如果允许,递归宏扩展会产生无限递归。尽管可能没有人预料到会有这样的用途,但事实证明,有一些方法可以使用递归宏扩展,不会导致无限递归(有关此问题的详细讨论,请参阅Boost预处理器库文档),但标准现在不会改变。

  2. 如果##应用于宏参数,则会抑制该参数的宏扩展。(参见GCC手册第3.5节"连接"或C标准,§6.10.3.3第2段。)抑制扩展是C标准的一部分,但GCC/Clang允许使用##有条件地抑制__VA_ARGS__前面的逗号的扩展是非标准的。(参见GCC手册第3.6节,变分宏。)显然,扩展仍然遵守标准关于不扩展串联宏参数的规则。

现在,关于可选逗号抑制的第二点,令人好奇的是,在实践中你几乎没有注意到它。您可以使用##来有条件地抑制逗号,并且参数仍将正常扩展:

#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. 首先,展开宏参数。(此展开位于调用宏的文本的上下文中。)这些展开的参数将替换宏替换体中的参数(但仅当参数不是###的参数时)。

  2. 然后将CCD_ 10和CCD_ 11运算符应用于替换令牌列表。

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

考虑到这一点,我们可以看到,在SHOW_ARGS(DOUBLE(2), DOUBLE(3))中,DOUBLE(2)在插入到替换令牌列表之前在步骤1中被扩展,而DOUBLE(3)在步骤3中被扩展为替换令牌列表的一部分。

这与SHOW_ARGS中的DOUBLE没有什么区别,因为它们是不同的宏。但是,如果它们是同一个宏,那么差异就会变得明显。

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

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

该宏创建了一个宏调用(或函数调用,但这里我们只对它是宏的情况感兴趣)。也就是说,in将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)

"当然",我们可以将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) }

这里,外部M0的第一个参数M0(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手册第3.6节,变分宏。)因此,如果你愿意依赖相对较新的编译器版本,有一个非常干净的解决方案:

#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. 这个问题最初是作为Variadic宏的副本而结束的:粘贴的令牌的扩展,但我认为这个答案并不适合这种特殊情况。

最新更新