具有零参数的可变参数宏即使使用## __ VA_ARGS__也不会编译

问题描述

如果我尝试编译以下代码

template <typename... TArgs>
void Dummy(const TArgs &...args)
{
}

#define DUMMY(...) Dummy("Hello",##__VA_ARGS__)

int main()
{
    DUMMY();
}

我收到以下编译错误

g++ -std=c++17 -O3 -Wall main.cpp && ./a.out
main.cpp: In function 'int main()':
main.cpp:6:48: error: expected primary-expression before ')' token
    6 | #define DUMMY(...) Dummy("Hello",##__VA_ARGS__)
      |                                                ^
main.cpp:10:5: note: in expansion of macro 'DUMMY'
   10 |     DUMMY();
      |     ^~~~~

https://coliru.stacked-crooked.com/a/c9217ba86e7d24bd

当我添加至少一个参数时,代码可以正常编译:

template <typename... TArgs>
void Dummy(const TArgs &...args)
{
}

#define DUMMY(dummy,...) Dummy(dummy,##__VA_ARGS__)

int main()
{
    DUMMY(); // This is strange. Why does this compile?
    DUMMY(1);
    DUMMY(1,2);
    DUMMY(1,2,3);
}

https://coliru.stacked-crooked.com/a/e30e14810d70f482

但是我不确定它是否正确,因为DUMMY至少接受一个参数,但是我传递了零。

解决方法

使用零参数时,标准__VA_ARGS__不会删除结尾的,。您的##__VA_ARGS__会删除多余的,是GCC扩展名。

此GCC扩展名不起作用,因为您使用的是标准兼容模式-std=c++17,而不是-std=gnu++17

,

关于C / C ++宏的一个重要事实是,不可能无参数地调用它们,因为允许宏参数为空的令牌序列。

因此,DUMMY()仅使用一个空参数而不是零参数来调用宏DUMMY。这解释了第二个示例为何起作用,也解释了第一个示例为何产生语法错误。

,##__VA_ARGS__没有任何元素时,GCC扩展名将从__VA_ARGS__中删除逗号。但是,单个空参数与没有参数不同。当您将DUMMY定义为#define DUMMY(...) 时,就保证了__VA_ARGS__至少有一个参数,因此,不会被删除。

***注意:如果您未使用--std选项指定某些ISO标准,则GCC会对该规则做例外处理。在这种情况下,如果...是唯一的宏参数,并且调用的参数为空,则,##__VA_ARGS__ 确实删除逗号。 CPP manual in the Variadic Marcos section中对此进行了说明:

上面的解释对于唯一的宏参数是可变参数参数的情况是模棱两可的,因为试图区分根本没有参数是空参数还是缺失参数毫无意义。符合特定的C标准时,CPP保留逗号。否则,逗号将作为标准的扩展名被删除。

DUMMY#define DUMMY(x,...)时,如果仅使用一个参数调用__VA_ARGS,则DUMMY将为空,其中包括两个调用DUMMY()(一个为空参数)和DUMMY(0)(一个参数0)。请注意,标准C和直到C ++ 20的C ++均不允许这种调用。他们要求至少有一个与省略号对应的参数(可能为空)。不过,GCC从未施加此限制,并且无论,##__VA_ARGS__设置如何,GCC都将省略--std逗号。

从C ++ 20开始,您可以使用__VA_OPT__内置宏作为处理逗号(以及可能需要删除的任何其他标点符号)的更标准方法。 __VA_OPT__还避免了上面带有空参数的问题,因为它使用了不同的标准:如果__VA_OPT__(x)包含至少一个令牌,x会扩展为__VA_ARGS__;否则,它将扩展为空序列。因此,__VA_OPT__可以按预期在此问题中的宏上工作。

我相信,至少在最新版本中,所有主流编译器现在都实现__VA_OPT__

,

由于某些原因(可能是GCC错误),如果仅使用https://github.com/guilherme-otran/heroku-buildpack-ffprobe.git而不使用其他参数,那么#define DUMMY(...)将无法按预期工作(如果{{1} }为空)。

仅当您使用##__VA_ARGS__进行编译时,才如此。使用__VA_ARGS__进行编译时,不会发生这种情况。但是无论如何,-std=c++17是GCC扩展,带有-std=gnu++17的代码完全不能与##__VA_ARGS__一起编译。但是,除非您设置##__VA_ARGS__标志,否则GCC允许在-std=c++17模式下使用GCC扩展名。但是似乎GCC扩展在-std=c++17-pedantic模式下的工作方式有所不同。

但是该问题可以解决:

-std=c++17

当您调用-std=gnu++17时,#include <utility> template <typename... TArgs> void Dummy(const TArgs &...args) { } namespace WA { class stub_t {}; stub_t ArgOrStub() { return {}; } template <typename T> auto ArgOrStub(T &&t) -> decltype( std::forward<T>(t) ) { return std::forward<T>(t); } template <typename... TArgs> void RemoveStubAndCallDummy(stub_t,TArgs &&...args) { Dummy(std::forward<TArgs>(args)...); } template <typename... TArgs> void RemoveStubAndCallDummy(TArgs &&...args) { Dummy(std::forward<TArgs>(args)...); } } #define DUMMY(first,...) WA::RemoveStubAndCallDummy( WA::ArgOrStub(first),##__VA_ARGS__ ) int main() { DUMMY(); } 参数将为空,经过预处理,我们将得到DUMMY(),它将返回first,随后将通过第一次重载来删除它WA::ArgOrStub()。它体积庞大,但我无法找到更好的解决方案。