我正在尝试学习可变参数模板和函数。我不明白为什么这段代码不能编译:
template<typename T>
static void bar(T t) {}
template<typename... Args>
static void foo2(Args... args)
{
(bar(args)...);
}
int main()
{
foo2(1, 2, 3, "3");
return 0;
}
当我编译时,它失败并出现错误:
错误 C3520:'args':参数包必须在此上下文中扩展
(在函数
foo2
中)。
可能发生包扩展的地方之一是在braced-init-list内部。您可以通过将扩展放入虚拟数组的初始值设定项列表来利用这一点:
template<typename... Args>
static void foo2(Args &&... args)
{
int dummy[] = { 0, ( (void) bar(std::forward<Args>(args)), 0) ... };
}
更详细地解释初始化程序的内容:
{ 0, ( (void) bar(std::forward<Args>(args)), 0) ... };
│ │ │ │ │
│ │ │ │ └─// pack expand the whole thing
│ │ │ │
│ │ └─// perfect forwarding └─// comma operator
│ │
│ └─// cast to void to ensure that regardless of bar()'s return type
│ // the built-in comma operator is used rather than an overloaded one
│
└─// ensure that the array has at least one element so that we don't try to make an
// illegal 0-length array when args is empty
演示。
在
{}
中扩展的一个重要优点是它保证了从左到右的评估。
使用C++17折叠表达式,你可以直接写
((void) bar(std::forward<Args>(args)), ...);
参数包只能在严格定义的上下文列表中扩展,而运算符
,
不是其中之一。换句话说,不可能使用包扩展来生成由一系列由运算符 ,
分隔的子表达式组成的表达式。
经验法则是“扩展可以生成由 ,
分隔的模式组成的
list,其中
,
是 list 分隔符。”运算符 ,
并不构造语法意义上的列表。
要为每个参数调用函数,可以使用递归(这是可变参数模板程序员的主要工具):
template <typename T>
void bar(T t) {}
void foo2() {}
template <typename Car, typename... Cdr>
void foo2(Car car, Cdr... cdr)
{
bar(car);
foo2(cdr...);
}
int main()
{
foo2 (1, 2, 3, "3");
}
参数包只能在严格定义的上下文列表中扩展,而运算符
,
不是其中之一。换句话说,不可能使用包扩展来生成由一系列由运算符 ,
分隔的子表达式组成的表达式。
经验法则是“扩展可以生成由
,
分隔的模式列表,其中 ,
是列表分隔符。”运算符 ,
并不构造语法意义上的列表。
要为每个参数调用函数,可以使用递归(这是可变参数模板程序员的主要工具):
#include <utility>
template<typename T>
void foo(T &&t){}
template<typename Arg0, typename Arg1, typename ... Args>
void foo(Arg0 &&arg0, Arg1 &&arg1, Args &&... args){
foo(std::forward<Arg0>(arg0));
foo(std::forward<Arg1>(arg1), std::forward<Args>(args)...);
}
auto main() -> int{
foo(1, 2, 3, "3");
}
有用的非复制信息
您可能在这个答案中没有看到的另一件事是使用
&&
说明符和 std::forward
。在 C++ 中,&&
说明符可以表示以下两种情况之一:右值引用或通用引用。
我不会讨论右值引用,而是讨论使用可变参数模板的人;通用参考是上帝赐予的。
完美转发
std::forward
和通用引用的用途之一是将类型完美转发到其他函数。
在您的示例中,如果我们将
int&
传递给 foo2
,由于模板推导后生成的 int
函数的签名,并且如果您想转发此 foo2
,它将自动降级为 arg
到另一个通过引用修改它的函数,您将得到不需要的结果(变量不会改变),因为 foo2
将传递对通过向其传递 int
创建的临时对象的引用。为了解决这个问题,我们指定一个转发函数来将 any 类型的 reference 传递给变量(右值 或 左值)。然后,为了确保我们传递在转发函数中传递的确切类型,我们使用std::forward
,然后并且only然后我们允许类型降级;因为我们现在正处于最重要的时刻。
C++17 的解决方案非常接近您预期的代码:
template<typename T>
static void bar(T t) {}
template<typename... Args>
static void foo2(Args... args) {
(bar(args), ...);
}
int main() {
foo2(1, 2, 3, "3");
return 0;
}
这会在每个表达式之间使用逗号运算符扩展模式
// imaginary expanded expression
(bar(1), bar(2), bar(3), bar("3"));
您可以使用
make_tuple
进行包扩展,因为它引入了一个上下文,其中扩展生成的 ,
序列是有效的
make_tuple( (bar(std::forward<Args>(args)), 0)... );
现在,我怀疑生成的未使用/未命名/临时零元组可以被编译器检测到并被优化掉。
这是一个完整的示例,基于此处的答案。
重现
console.log
的示例,如 JavaScript 中所示:
Console console;
console.log("bunch", "of", "arguments");
console.warn("or some numbers:", 1, 2, 3);
console.error("just a prank", "bro");
文件名例如
js_console.h
:
#include <iostream>
#include <utility>
class Console {
protected:
template <typename T>
void log_argument(T t) {
std::cout << t << " ";
}
public:
template <typename... Args>
void log(Args&&... args) {
int dummy[] = { 0, ((void) log_argument(std::forward<Args>(args)),0)... };
cout << endl;
}
template <typename... Args>
void warn(Args&&... args) {
cout << "WARNING: ";
int dummy[] = { 0, ((void) log_argument(std::forward<Args>(args)),0)... };
cout << endl;
}
template <typename... Args>
void error(Args&&... args) {
cout << "ERROR: ";
int dummy[] = { 0, ((void) log_argument(std::forward<Args>(args)),0)... };
cout << endl;
}
};