即使使用 ##__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
如果我尝试编译以下代码:
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