我正在使用第三方UI库开发一个程序,其中包含Vbox(void *first, ...)
形式的函数。它们用作布局函数并采用任意数量的参数。列表的末尾由检测到的第一个NULL定义。这意味着我需要记住以NULL结束我的列表,这是我经常做不到的事情。
所以我创建了一些辅助宏,它们应该展开以使我的列表附加一个NULL。
这些形式如下:
#define UtlVbox(first, ...) Vbox(first, ##__VA_ARGS__, NULL)
##
之前的__VA_ARGS__
用于摆脱之前的逗号__VA_ARGS
为空。
如果框实际上应该初始化为空(first
),我需要Vbox(NULL)
:在这些情况下,用户必须显式添加NULL,因为我无法摆脱{{ 1 {}在,
之后(因为__VA_ARGS__
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
除了最里面的UtlHbox之外,所有内容都按预期精确扩展,由于某种原因,UtlHbox没有被扩展,因此会在编译时抛出错误。 (另外,在这个例子中没有展开NULL,因为没有任何相关的#include)。在VC12(Visual Studio 2013)中,事情编译得很好。
这里发生了什么?这是# 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);
}
操作的不同含义之间的冲突吗?有什么方法可以解决这个问题吗?
我正在使用GCC 4.6.3,但我尝试使用GCC 7.1在GodBolt上编译它并获得相同的结果。
经过一番研究,我开始认为我已经碰到known problem in GCC。
似乎海湾合作委员会正在崛起。如果我创建第三个宏##
并使用这个新宏替换上面示例中的内部UtlHbox,输出正确:
#define UtlZbox(first, ...) Zbox(first , ##__VA_ARGS__, NULL)
当可变参数宏在另一个实例中重复时,似乎GCC会自行跳过。
我做了一些其他测试(修改宏以简化可视化):
static void* Test()
{
return Hbox(Foo, Vbox(Zbox(Bar, NULL), NULL), NULL);
}
这是Godbolt's output,使用GCC 7.1进行编译(在我的机器上使用4.6.3进行编译,输出相同):
成功的转化用绿色箭头标记,失败是红色。问题似乎是当具有可变参数的宏X放在X的另一个实例的可变参数中的任何位置时(即使作为某个其他宏Y的参数(可变参数或非参数))。
最后一个测试块(标记为#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;
}
)是所有先前失败的案例的重复,只替换了无法使用UtlZbox扩展的宏。除了将UtlZbox放置在另一个UtlZbox的可变参数中之外,这样做会导致每个案例的正确扩展。
答案 0 :(得分:2)
这不是一个错误;这是&#34;蓝色油漆&#34;。
在VC12(Visual Studio 2013)中,事情编译得很好。
提一下...... Visual Studio的预处理器是非标准的。
我遇到了一个我无法理解的奇怪情况。
......在这里我可以提供帮助。首先,让我们回顾一下预处理器如何工作的规则。
宏的功能扩展分为多个步骤,我们可以称之为
在参数识别期间,您只需将正式参数与调用的参数匹配即可。对于不同的参数宏,标准要求变量参数本身具有一个或多个被调用的参数。
...作为gnu扩展(你正在使用),我们可以将变化部分映射到无参数。我将打电话给 null 。请注意,这与 empty (和占位符标记)不同;特别是,如果我们#define FOO(x,...)
,则调用FOO(z)
将__VA_ARGS__
设置为 null ;相比之下,FOO(z,)
会将其设置为为空。
在参数替换期间,您应用替换列表;在替换列表中,您可以使用调用的参数替换形式参数。在这样做之前,不被字符串化的任何被调用的参数以及不参与粘贴操作符(粘贴的左侧或右侧)都是完全展开的。
按照任何顺序,接下来应用字符串化和粘贴。
执行上述步骤后,在重新扫描和进一步更换步骤中再进行一次最终扫描。作为一项特殊规则,在针对特定宏的扫描期间,您不再允许扩展该宏。对此的标准术语是&#34;蓝色油漆&#34 ;;对于此扩展,宏被标记(或&#34;涂成蓝色&#34;)。整个扫描完成后,宏就会没有上映&#34;。
让我们来看你的第一个例子,但我会稍微改变一下:
#define UtlVbox(first, ...) Vbox(first, ##__VA_ARGS__, NULL)
#define UtlHbox(first, ...) Hbox(first, ##__VA_ARGS__, NULL)
#define foomacro Foo
UtlHbox(foomacro,UtlVbox(UtlHbox(Bar)))
在这里,我只是采取了&#34; C&#34;离开只关注预处理器。我还更改了调用以调用宏foomacro
以突出显示某些内容。现在,我们来看看UtlHbox调用是如何扩展的。
我们从参数识别开始。 UtlHbox
有正式论据first
和...
;调用具有参数foomacro
和UtlVbox(UtlHbox(Bar))
。因此first
为foomacro
而__VA_ARGS__
为UtlVbox(UtlHbox(Bar))
。
接下来,我们使用替换列表执行参数替换,即:
Hbox(first, ##
__VA_ARGS__
, NULL)
...所以我们在__VA_ARGS__
展开后将first
替换为foomacro
;和foomacro
__VA_ARGS__
字面意思。后一种情况不同,因为在此替换列表中,UtlVbox(UtlHbox(Bar))
是粘贴运算符的参与者(即右侧);因此,它不会扩展。所以我们得到了这个:
__VA_ARGS__
接下来,我们执行字符串化和粘贴,获取此信息:
Hbox(Foo, ## UtlVbox(UtlHbox(Bar)))
接下来,我们对Hbox(Foo, UtlVbox(UtlHbox(Bar)))
应用重新扫描并进一步替换。所以我们绘制UtlHbox
蓝色,然后我们评估该字符串。你可能已经看到自己在这里遇到了麻烦,但为了完成我将继续前进。
在重新扫描和进一步替换期间,我们找到UtlHbox
,这是另一个宏。这为宏UtlVbox
提供了第二级评估。
在第二级参数标识中,UtlVbox
为first
;并且UtlHbox(Bar)
null 。
在第二级论据替换中,我们会查看__VA_ARGS__
的替换列表,即:
UtlVbox
由于Vbox(first, ##__VA_ARGS__, NULL)
未被字符串化或粘贴,我们在替换之前评估调用的参数first
。 但是由于UtlHbox被涂成蓝色,我们不认为它是一个宏。同时,UtlHbox(Bar)
为空。所以我们得到:
__VA_ARGS__
在粘贴时的第二级,我们会使用 null 将逗号放置标记粘贴到逗号右侧;这会触发逗号省略规则的gnu扩展,因此生成的粘贴会删除逗号,我们得到:
Vbox(UtlHbox(Bar), ## null, NULL)
在第二级重新扫描和替换中,我们将Vbox(UtlHbox(Bar), NULL)
蓝色绘制,然后重新扫描这一块。由于UtlVbox
仍然涂成蓝色,因此 仍然无法识别为宏。由于没有其他东西是宏,扫描完成。
因此退出一个级别,我们结束了这个:
UtlHbox
...在继续之前,重新扫描并替换每一个,我们会重新点击Hbox(Foo, Vbox(UtlHbox(Bar), NULL))
和UtlVbox
。
有什么方法可以解决这个问题吗?
嗯,请注意,有两个级别的扩展;一个发生在参数替换期间,另一个发生在重新扫描和替换期间。前者发生在蓝色涂料应用之前,并且它可以无限递归:
UtlHbox
...很乐意扩展到:
#define BRACIFY(NAME_) { NAME_ }
BRACIFY(BRACIFY(BRACIFY(BRACIFY(BRACIFY(Z)))) BRACIFY(X))
这看起来像你想要做的。但是&#34;论证替代&#34;只有在您的参数没有字符串化或粘贴时才会进行评估。所以,真正杀死你的是gnu逗号省略功能;您对此的使用涉及将粘贴运算符应用于{ { { { { Z } } } } { X } }
;这取消了你在论证替代期间扩大的不同论点。相反,它们只能在重新扫描和替换期间进行扩展,而在那个阶段,您的宏被涂成蓝色。
所以解决方案是简单地避免使用逗号。在你的案例中,这实际上非常简单。让我们仔细看看:
__VA_ARGS__
因此,您希望#define UtlVbox(first, ...) Vbox(first, ##__VA_ARGS__, NULL)
#define UtlHbox(first, ...) Hbox(first, ##__VA_ARGS__, NULL)
成为UtlVbox(a)
,Vbox(a, NULL)
成为UtlVbox(a, b)
。那么这样做呢?
Vbox(a, b, NULL)
现在这个:
#define UtlVbox(...) Vbox(__VA_ARGS__, NULL)
#define UtlHbox(...) Hbox(__VA_ARGS__, NULL)
...扩展为:
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)));
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)));
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)));
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)));