【发布时间】:2017-05-25 15:11:49
【问题描述】:
我正在使用带有Vbox(void *first, ...) 形式的函数的第三方UI 库开发程序。这些用作布局函数并采用任意数量的参数。列表的结尾由检测到的第一个 NULL 定义。这意味着我需要记住以 NULL 结束我的列表,这是我经常做不到的。
所以我创建了一些辅助宏,它们应该扩展为我的列表附加一个 NULL。
这些是以下形式:
#define UtlVbox(first, ...) Vbox(first, ##__VA_ARGS__, NULL)
__VA_ARGS__ 之前的## 用于去掉前面的逗号,以防__VA_ARGS 为空。
我需要first,以防框实际上应该初始化为空(Vbox(NULL)):在这些情况下,用户必须显式添加 NULL,因为我无法摆脱 @ 之后的 , 987654335@(因为## hack 只在逗号在## 之前才有效,而不是在之后),因此用户必须给出明确的 NULL,这将导致以下扩展:Vbox(NULL, NULL),即有点多余但很好。
这总体上效果很好,但我遇到了一个我不太理解的奇怪情况。
以如下文件为例:
// expand.c
void* Vbox(void* first, ...);
void* Hbox(void* first, ...);
#define UtlVbox(first, ...) Vbox(first, ##__VA_ARGS__, NULL)
#define UtlHbox(first, ...) Hbox(first, ##__VA_ARGS__, NULL)
static void* Test()
{
return UtlHbox(
Foo,
UtlVbox(
UtlHbox(Bar)));
}
如果我运行gcc -E expand.c,我会得到以下输出:
# 1 "expand.c"
# 1 "<built-in>"
# 1 "<command-line>"
# 1 "expand.c"
void* Vbox(void* first, ...);
void* Hbox(void* first, ...);
static void* Test()
{
return Hbox(Foo, Vbox(UtlHbox(Bar), NULL), NULL);
}
除了最里面的 UtlHbox 之外,一切都按照预期进行了精确扩展,由于某种原因,它没有被扩展,因此在编译时会引发错误。 (此外,在此示例中未扩展 NULL,因为没有任何相关的#include)。在 VC12 (Visual Studio 2013) 中,编译得很好。
这里发生了什么?这是##操作的不同含义之间的冲突吗?有没有办法解决这个问题?
我使用的是 GCC 4.6.3,但我尝试在 GodBolt 上使用 GCC 7.1 进行编译并得到相同的结果。
经过一番研究,我开始认为我遇到了known problem in GCC。
似乎 GCC 自己在绊倒。如果我创建第三个宏
#define UtlZbox(first, ...) Zbox(first , ##__VA_ARGS__, NULL)
并用这个新宏替换上例中的内部UtlHbox,输出是正确的:
static void* Test()
{
return Hbox(Foo, Vbox(Zbox(Bar, NULL), NULL), NULL);
}
当可变参数宏在其自身的另一个实例中重复时,GCC 似乎会绊倒自己。
我做了一些其他测试(修改宏以简化可视化):
#define UtlVbox(first, ...) V(first,##__VA_ARGS__)
#define UtlHbox(first, ...) H(first,##__VA_ARGS__)
int main()
{
// HHH
UtlHbox(UtlHbox(UtlHbox(1)));
UtlHbox(UtlHbox(UtlHbox(2, 1)));
UtlHbox(UtlHbox(2, UtlHbox(1)));
UtlHbox(2, UtlHbox(UtlHbox(1)));
UtlHbox(3, UtlHbox(2, UtlHbox(1)));
// HHV
UtlHbox(UtlHbox(UtlVbox(1)));
UtlHbox(UtlHbox(UtlVbox(2, 1)));
UtlHbox(UtlHbox(2, UtlVbox(1)));
UtlHbox(2, UtlHbox(UtlVbox(1)));
UtlHbox(3, UtlHbox(2, UtlVbox(1)));
// HVH
UtlHbox(UtlVbox(UtlHbox(1)));
UtlHbox(UtlVbox(UtlHbox(2, 1)));
UtlHbox(UtlVbox(2, UtlHbox(1)));
UtlHbox(2, UtlVbox(UtlHbox(1)));
UtlHbox(3, UtlVbox(2, UtlHbox(1)));
// VHH
UtlVbox(UtlHbox(UtlHbox(1)));
UtlVbox(UtlHbox(UtlHbox(2, 1)));
UtlVbox(UtlHbox(2, UtlHbox(1)));
UtlVbox(2, UtlHbox(UtlHbox(1)));
UtlVbox(3, UtlHbox(2, UtlHbox(1)));
return 0;
}
这是Godbolt's output,使用 GCC 7.1 编译(在我的机器上使用 4.6.3 进行编译会得到相同的输出):
成功的转换用绿色箭头标记,失败用红色箭头标记。问题似乎是当带有可变参数的宏 X 被放置在另一个 X 实例的可变参数中的任何位置时(即使作为某个其他宏 Y 的参数(无论是否可变))。
最后一个测试块(标记为// Failures...)是之前所有失败案例的重复,只是用 UtlZbox 替换未能扩展的宏。除了将 UtlZbox 放置在另一个 UtlZbox 的可变参数中的情况外,这样做会导致在每种情况下都进行适当的扩展。
【问题讨论】:
-
看起来不是一个好主意。你为什么不把一个带有列表的数组传递给你的函数呢?这将为构建内部结构、传递参数列表等节省大量运行时开销。
-
@Olaf:你的意思是原来的函数是
Vbox(first, ...)的形式吗?不幸的是,图书馆不是我的,它的使用是不可协商的。 -
是的,我就是这个意思。而且这个界面并不能很好地说明图书馆。
-
解决方案不是包装库,而是确保您始终正确调用它...
-
@Alnitak:但据我了解,模板不需要其他参数。这不是
##的重点吗?当前缀一个空的__VA_ARGS__时,它会删除前面的逗号,对吗(见here)?在这种情况下,UtlHbox(oneParam)应该简单地变成Hbox(oneParam, NULL)(而不是(oneParam, , NULL)。事实上,这就是 UtlVbox 中发生的事情,它只有一个参数(UtlHbox),但处理得很好,去掉了不必要的逗号并在末尾附加一个 NULL。
标签: c gcc macros variadic-macros