即使使用 ##__VA_ARGS__ 也无法编译具有零参数的可变参数宏

Variadic macros with zero arguments doesn't compile even with ##__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

出于某些原因(这可能是 GCC 错误),如果您只使用 #define DUMMY(...) 而没有其他参数,那么 ##__VA_ARGS__ 将不会按预期工作(如果 [= 它不会删除逗号) 13=] 为空)。

仅当您使用 -std=c++17 编译时才成立。当你用 -std=gnu++17 编译时,这不会发生。但无论如何 ##__VA_ARGS__ 是 GCC 扩展,带有 ##__VA_ARGS__ 的代码根本不能用 -std=c++17 编译。但是 GCC 允许在 -std=c++17 模式下使用 GCC 扩展,除非你设置 -pedantic 标志。但似乎 GCC 扩展在 -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(),这将 return stub_t 稍后将得到被 RemoveStubAndCallDummy 的第一次重载删除。它很笨重,但我找不到更好的解决方案。

关于 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:

中注明

The above explanation is ambiguous about the case where the only macro parameter is a variable arguments parameter, as it is meaningless to try to distinguish whether no argument at all is an empty argument or a missing argument. CPP retains the comma when conforming to a specific C standard. Otherwise the comma is dropped as an extension to the standard.

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

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

我相信所有主要编译器现在都实现了 __VA_OPT__,至少在它们的最新版本中是这样。

C++20 引入了 __VA_OPT__ 作为一种在参数数量大于零的情况下可选地扩展可变参数宏中标记的方法。
这消除了对 ##__VA_ARGS__ GCC 扩展的需要。 如果您可以使用该版本的标准,那应该是一个优雅的 compiler-unspecific 解决方案。

The sequence __VA_OPT__(x), which is only legal in the substitution list of a variable-argument macro, expands to x if __VA_ARGS__ is non-empty and to nothing if it is empty.

所以你可以简单地做:
#define DUMMY(...) Dummy("Hello" __VA_OPT__(,) __VA_ARGS__)

这是一篇很好的博客文章,其中包含 __VA_OPT__ 的实际应用(以及更多关于预处理器宏的内容): https://www.scs.stanford.edu/~dm/blog/va-opt.html