可变参数模板(Variadic templates)是C++11新增的最强大的特性之一,它对参数高度泛化,能够让我们创建可以接受可变参数的函数模板和类模板。
template
void func(T... args)
{
//...
}
上面的我们把带…的模板参数称为模板参数包(template parameter pack)
上面这个函数模板的参数 args
前面有省略号,我们称之为模板参数包(template parameter pack)的可变模版参数,它里面包含了0到N个模版参数,而我们是无法直接获取 args
中的每个参数的,只能通过展开参数包的方式来获取参数包中的每个参数。
递归展开的方式如下:
template
void foo(T first, Args... args)
{
cout << first << " ";
foo(args...);
}
//int main()
//foo("good", 2, "hello", 4, 110);
我们写出如上一个带可变模板参数的函数foo,我们尝试在main函数中调用发现无法调用
有递归自然有出口,当函数包空时,仍会递归一个空包作为参数,而我们没有空参数函数,所以我们还要再增加一个空参数的函数来进行特化
如下:
void foo()
{
cout << endl;
}
template
void foo(T first, Args... args)
{
cout << first << " ";
foo(args...);
}
int main()
{
foo(1, 2, 3, 4);
foo("good", 2, "hello", 4, 110);
return 0;
}
//输出
//1 2 3 4
//good 2 hello 4 110
当然我们可以规定递归出口为其他数量的参数,如:
void foo(int a)
{
cout << endl;
}
template
void foo(T first, Args... args)
{
cout << first << " ";
foo(args...);
}
int main()
{
foo(1, 2, 3, 4);
foo("good", 2, "hello", 4, 110);
return 0;
}
//输出
//1 2 3
//good 2 hello 4
当然,当我们定义递归出口为一个参数的函数时,我们调用foo必须传入不少于一个参数
我们其实是可以计算参数包的大小的,如sizeof…(args)
void foo()
{
cout << endl;
}
template
void foo(T first, Args... args)
{
cout << first << " " << sizeof...(args) << endl;
foo(args...);
}
int main()
{
foo(1, 2, 3, 4);
return 0;
}
//输出
//1 3
//2 2
//3 1
//4 0
那么我们是否可以通过对参数包大小的判断来结束函数递归,从而省去无参数或者少参数函数作为递归出口呢?
template
void foo(T first, Args... args)
{
cout << first << " " << sizeof...(args) << endl;
if (!sizeof...(args))
return;
foo(args...);
}
我们发现直接报错了,也就是说这种方式不可行
逗号表达式展开包其实是利用了C++11新特性,列表初始化。
我们列表初始化的原理就是先用列表构建initializer_list,再用initializer_list去构建我们的容器
如果我们把参数包放入初始化列表中会怎样呢?
template
void foo(Args... args)
{
initializer_list a{args...};
for (auto x : a)
cout << x << " ";
}
//输出
//1 2 3 4
我们发现参数包放入初始化列表中,由于初始化列表从左往右执行,参数包中的参数会被逐个取出,此时由于没有递归展开,所以我们不需要再额外定义空参数的重载函数,传入参数也没有数目限制。
利用初始化列表和逗号表达式结合,我们可以如下展开参数包:
template
void foo(Args... args)
{
(void)initializer_list{(cout << args << " ", 0)...};
}
int main()
{
foo(1, 2, 3, 4, "GenshinImpact", 3.14);
return 0;
}
//输出
//1 2 3 4 GenshinImpact 3.14
我们发现很顺利的输出了,甚至不受类型限制
其实剖析一下发现逗号表达式是一种很犯规的写法,我们逗号表达式的返回值是最右边的表达式,也就是0,所以最终用来初始化列表的元素是0,但是由于列表初始化要从左向右执行,所以我们的参数包会被展开,假如参数包有N个参数,我们展开N次,但是此次返回值都是0,所以得到了N个0的列表,而参数包内的内容都被输出了。
enable_if是C++11新引入的一个结构体,定义如下:
// Primary template.
/// Define a member typedef @c type only if a boolean constant is true.
template
struct enable_if
{ };
// Partial specialization for true.
template
struct enable_if
{ typedef _Tp type; };
我们可以看出下面是上面的一个偏特化。当我们传入第一个参数为true时会用第二个模板来实例化,将_Tp typedef为type,而第一个模板什么也没做。
故而enable_if常用于需要根据不同的类型的条件实例化不同模板的情形。也就是说,在不同条件下选用不同类型,其广泛的应用在 C++ 的模板元编程(meta programming)之中,利用的就是SFINAE原则,英文全称为Substitution failure is not an error,意思就是匹配失败不是错误,假如有一个特化会导致编译时错误,只要还有别的选择,那么就无视这个特化错误而去选择另外的实现。
因而我们可以借此来解决我们递归展开函数包递归出口函数和参数限制的问题。
具体流程就是:
代码如下:
template
typename enable_if::value>::type _foo(const tup &t)
{
cout << endl;
}
template
typename enable_if < k::value>::type _foo(const tup &t)
{
cout << get(t) << " ";
_foo(t);
}
template
void foo(Args... args)
{
_foo<0>(make_tuple(args...));
}
int main()
{
foo(2023, "GenshinImpact", "hello", 2024);
return 0;
}
优雅,实在是太优雅了。
前面几种都是C++11的内容,而我们的折叠表达式(Fold Expressions)则是我们C++17的新语法特性,使用折叠表达式可以简化对C++11中引入的参数包的处理,可以在某些情况下避免使用递归,更加方便的展开参数。
如下示例:
template
void foo(Args... args)
{
(cout << ... << args) << endl;
}
int main()
{
foo(2023, "GenShinImpact", "hello");
return 0;
}
简洁了不少,但是如何格式化呢?需要增加格式化辅助函数。
template
string format(const T &t)
{
stringstream ss;
ss << " " << t << " ";
return ss.str();
}
template
void foo(Args... args)
{
(cout << ... << format(args)) << endl;
}
int main()
{
foo(2023, "GenShinImpact", "hello");
return 0;
}
//输出
// 2023 GenShinImpact hello
也可以直接利用逗号表达式进行简化
template
void foo(Args... args)
{
(cout << ... << (cout << args, " ")) << endl;
}
int main()
{
foo(2023, "GenShinImpact", "hello");
return 0;
}
//输出
//2023 GenShinImpact hello
括号里逗号表达式的返回值是" “,当输出完从参数包里拆出的args,返回” "给左边的输出流输出
可变参数模板参数高度泛化,提高了编程的泛用性。
而为了实现可变参数模板我们引入了参数包,于是需要对参数包进行展开,我们可以: