首页 > 解决方案 > 在 ## 运算符存在的情况下,可变参数 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相同)

标签: cgccc-preprocessorvariadic-macros

解决方案


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

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

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

这两个原则如下:

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

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

  2. 然后将#and##运算符应用于替换令牌列表。

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

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

DOUBLE这与inside没有区别SHOW_ARGS,因为它们是不同的宏。但是,如果它们是相同的宏,差异就会变得明显。

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

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

“当然”,我们可以将INVOKE其扩展为INVOKE

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

⇒ Arguments are (Arguments are (one arg))

这很好用,因为没有##inside 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) }

在这里,outer 的第一个参数M0,M0(1,2)不与 一起使用##,因此它作为调用的一部分被扩展。其他两个参数是 的一部分__VA_ARGS__,与 一起使用##。因此,它们在被替换到宏的替换列表之前不会被扩展。但作为宏替换列表的一部分,它们的扩展被 no-recursive-macros 规则抑制。

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 实现__VA_OPT__了 C 和 C++。(请参阅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 macros:expansion of pasteed tokens的副本而关闭的,但我认为这个答案并不适合这种特殊情况。


推荐阅读