【问题标题】:gcc failing to expand some macrosgcc 未能扩展某些宏
【发布时间】: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


【解决方案1】:

这不是错误;这是“蓝色油漆”。

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

顺便提一下……Vis​​ual Studio 的预处理器是非标准的。

我遇到了一个我不太理解的奇怪情况。

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

大纲

类似宏的功能扩展发生在多个步骤中,我们可以称之为

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

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

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

在参数替换期间,您应用替换列表;在替换列表中,您可以用调用的参数替换形式参数。在这样做之前,任何调用的参数,如果 not 被字符串化并且 not 参与粘贴操作符(无论是粘贴的左侧还是右侧),都将完全展开。 p>

接下来以任意顺序应用字符串化和粘贴。

执行上述步骤后,在重新扫描和进一步替换步骤期间,还会再进行一次最终扫描。作为一项特殊规则,在此扫描特定宏期间,您不再被允许扩展同一个宏。标准术语是“蓝色油漆”;该扩展标记了宏(或“涂成蓝色”)。整个扫描完成后,宏就“未绘制”。

解释

让我们以您的第一个示例为例,但我将对其稍作改动:

#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>__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仍然被涂成蓝色,它仍然未被识别为宏。由于没有其他东西是宏,因此扫描完成。

所以退出一个关卡,我们结束了:

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

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

解决方案

有没有办法解决这个问题?

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

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

...会很高兴地扩展为:

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

这看起来像你想要做的。但是“参数替换”评估仅在您的参数没有字符串化或粘贴时才会发生。所以在这里真正要你命的是 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);

【讨论】:

  • 可靠的答案!正如 OP 中提到的,我用first 定义宏的原因是因为我还需要处理应该将框声明为空的情况(即Vbox(NULL))。如果没有first,宏可以称为UtlVbox(),但它会扩展为Vbox(, NULL),因为我不能省略逗号after __VA_ARGS__。使用first 会通知用户至少应该为宏提供一个参数(即使它的NULL)。
  • 话虽如此,我刚刚意识到删除first 并使用这些宏仍然比使用函数本身更好。没有first,如果用户调用UtlVbox(),就会出现编译错误,很难理解(抱怨用户在自己的代码中看不到逗号)。然而,函数本身确实很危险,因为忘记添加 NULL 编译就好了,但会导致运行时错误。我认为难以解析的编译错误比运行时错误更好!
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多