gcc 未能扩展某些宏

gcc failing to expand some macros

我正在使用第三方 UI 库开发一个程序,其函数形式为 Vbox(void *first, ...)。这些用作布局函数并采用任意数量的参数。列表的末尾由检测到的第一个 NULL 定义。这意味着我需要记住以 NULL 结束我的列表,这是我经常做不到的事情。

所以我创建了一些辅助宏,它们应该展开以在我的列表中附加一个 NULL。

它们的形式是:

#define UtlVbox(first, ...) Vbox(first, ##__VA_ARGS__, NULL)

__VA_ARGS__ 之前的 ## 用于去掉前面的逗号,以防 __VA_ARGS 为空。

我需要 first 以防盒子实际初始化为空 (Vbox(NULL)):在这些情况下,用户必须显式添加 NULL,因为我无法摆脱,__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,我得到以下输出:

# 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 外,所有内容都按预期精确扩展,由于某种原因未扩展,因此在编译时抛出错误。 (此外,由于没有任何相关的#include,因此在此示例中未扩展 NULL)。在 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 的可变参数中的情况。

这不是错误;这是 "blue paint".

In VC12 (Visual Studio 2013), things compile just fine.

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

I've bumped into an odd situation I can't quite understand.

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

大纲

像宏一样的函数展开有很多步骤,我们可以称之为

  1. 参数识别
  2. 参数替换
  3. 字符串化和粘贴
  4. 正在重新扫描并进一步替换

在参数识别过程中,您只需将形式参数与调用的参数进行匹配。对于可变参数宏,标准 要求可变参数本身具有一个或多个调用参数。

...作为 gnu 扩展(您正在使用),我们可以将变化部分映射到无参数。我将其称为 null。请注意,这与 empty(和占位符标记)不同;特别是,如果我们 #define FOO(x,...),那么调用 FOO(z) 会将 __VA_ARGS__ 设置为 null;相比之下,FOO(z,) 会将其设置为 empty.

在参数替换期间,您应用替换列表;在替换列表中,您可以用调用的参数替换形式参数。在这样做之前,任何被调用的 not 参数被字符串化并且 not 参与粘贴运算符(粘贴的左侧或右侧)已完全展开。

接下来应用字符串化和粘贴,顺序不限。

执行完上述步骤后,在重新扫描和进一步替换步骤中还会进行最后一次扫描。作为一项特殊规则,在扫描特定宏的过程中,您不再被允许展开同一个宏。标准行话是"blue paint";该宏被标记(或 "painted blue")用于此扩展。整个扫描完成后,宏为 "unpainted".

解释

让我们以您的第一个示例为例,但我将稍微更改一下:

#define UtlVbox(first, ...) Vbox(first, ##__VA_ARGS__, NULL)
#define UtlHbox(first, ...) Hbox(first, ##__VA_ARGS__, NULL)
#define foomacro Foo
UtlHbox(foomacro,UtlVbox(UtlHbox(Bar)))

这里我只是把 "C" 去掉,只关注预处理器。我还更改了调用宏 foomacro 来突出显示某些内容的调用。下面是 UtlHbox 调用的扩展方式。

我们从参数识别开始。 UtlHbox 有形式参数 first...;调用有参数 foomacroUtlVbox(UtlHbox(Bar))。所以 firstfoomacro__VA_ARGS__UtlVbox(UtlHbox(Bar)).

接下来我们使用替换列表执行参数替换,即:

Hbox(<b>first</b>, ##<b><code>__VA_ARGS__, NULL)

...所以我们将 first 替换为 foomacro foomacro 展开后 __VA_ARGS__UtlVbox(UtlHbox(Bar)) 字面上的 。后一种情况不同,因为在这个替换列表中,__VA_ARGS__ 是粘贴运算符的参与者(即右侧);因此,它不会展开。所以我们得到这个:

Hbox(Foo, ## UtlVbox(UtlHbox(Bar)))

接下来我们进行字符串化和粘贴,得到:

Hbox(Foo, UtlVbox(UtlHbox(Bar)))

接下来我们对UtlHbox应用重新扫描和进一步替换。所以我们将 UtlHbox 涂成蓝色,然后计算该字符串。你可能已经看到自己在这里遇到麻烦了,但为了完成我会继续。

在重新扫描和进一步替换的过程中,我们发现UtlVbox,这是另一个宏。这会产生宏 UtlVbox.

的第二级评估

二级参数识别中,firstUtlHbox(Bar)__VA_ARGS__null.

第二层参数替换,我们看UtlVbox的替换列表,即:

Vbox(first, ##__VA_ARGS__, NULL)

由于 first 未被字符串化或粘贴,我们在替换它之前评估调用的参数 UtlHbox(Bar)但是由于UtlHbox被涂成蓝色,我们无法将其识别为宏。同时,__VA_ARGS__ 为空。所以我们简单地得到:

Vbox(UtlHbox(Bar), ## <i>null</i>, NULL)

在粘贴时的第二层,我们将放置标记粘贴到带有 null 的逗号右侧;这会触发逗号省略规则的 gnu 扩展,因此生成的粘贴会删除逗号,我们得到:

Vbox(UtlHbox(Bar), NULL)

第二层重扫替换,我们把UtlVbox涂成蓝色,然后再重扫这块。由于 UtlHbox still 涂成蓝色,因此 still 未被识别为宏。由于没有别的是宏,扫描完成。

所以退出一个级别,我们结束了这个:

Hbox(Foo, Vbox(UtlHbox(Bar), NULL))

...在继续之前,完成每个的重新扫描和替换,我们取消绘制 UtlVboxUtlHbox

解决方案

Is there any way to solve this?

嗯,请注意有两个级别的扩展;一个发生在参数替换期间,另一个发生在重新扫描和替换期间。前者发生在蓝色油漆应用之前,并且它可以无限递归:

#define BRACIFY(NAME_) { NAME_ }
BRACIFY(BRACIFY(BRACIFY(BRACIFY(BRACIFY(Z)))) BRACIFY(X))

...将愉快地扩展到:

{ { { { { Z } } } } { X } }

这看起来像你想要做的。但是 "argument substitution" 评估仅在您的参数未进行字符串化或粘贴时才会发生。所以这里真正让你丧命的是 gnu 逗号省略功能;您对它的使用涉及将粘贴运算符应用于 __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)));

...扩展为:

Hbox(Hbox(Hbox(1, NULL), NULL), NULL);
Hbox(Hbox(Hbox(2, 1, NULL), NULL), NULL);
Hbox(Hbox(2, Hbox(1, NULL), NULL), NULL);
Hbox(2, Hbox(Hbox(1, NULL), NULL), NULL);
Hbox(3, Hbox(2, Hbox(1, NULL), NULL), NULL);
Hbox(Hbox(Vbox(1, NULL), NULL), NULL);
Hbox(Hbox(Vbox(2, 1, NULL), NULL), NULL);
Hbox(Hbox(2, Vbox(1, NULL), NULL), NULL);
Hbox(2, Hbox(Vbox(1, NULL), NULL), NULL);
Hbox(3, Hbox(2, Vbox(1, NULL), NULL), NULL);
Hbox(Vbox(Hbox(1, NULL), NULL), NULL);
Hbox(Vbox(Hbox(2, 1, NULL), NULL), NULL);
Hbox(Vbox(2, Hbox(1, NULL), NULL), NULL);
Hbox(2, Vbox(Hbox(1, NULL), NULL), NULL);
Hbox(3, Vbox(2, Hbox(1, NULL), NULL), NULL);
Vbox(Hbox(Hbox(1, NULL), NULL), NULL);
Vbox(Hbox(Hbox(2, 1, NULL), NULL), NULL);
Vbox(Hbox(2, Hbox(1, NULL), NULL), NULL);
Vbox(2, Hbox(Hbox(1, NULL), NULL), NULL);
Vbox(3, Hbox(2, Hbox(1, NULL), NULL), NULL);