gcc无法扩展某些宏

时间:2017-05-25 15:11:50

标签: c gcc macros variadic-macros

我正在使用第三方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进行编译,输出相同):

enter image description here

成功的转化用绿色箭头标记,失败是红色。问题似乎是当具有可变参数的宏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的可变参数中之外,这样做会导致每个案例的正确扩展。

1 个答案:

答案 0 :(得分:2)

这不是一个错误;这是&#34;蓝色油漆&#34;。

在VC12(Visual Studio 2013)中,事情编译得很好。

提一下...... Visual Studio的预处理器是非标准的。

我遇到了一个我无法理解的奇怪情况。

......在这里我可以提供帮助。首先,让我们回顾一下预处理器如何工作的规则。

概要

宏的功能扩展分为多个步骤,我们可以称之为

  1. 参数识别
  2. 参数替换
  3. 字符串化和粘贴
  4. 重新扫描并进一步更换
  5. 在参数识别期间,您只需将正式参数与调用的参数匹配即可。对于不同的参数宏,标准要求变量参数本身具有一个或多个被调用的参数。

    ...作为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...;调用具有参数foomacroUtlVbox(UtlHbox(Bar))。因此firstfoomacro__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提供了第二级评估。

    在第二级参数标识中,UtlVboxfirst;并且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)));