一个可变参数模板(variadic template)就是一个接受可变数目参数的函数模板或类模板。
可变数目的参数被称为参数包(parameter packet)。
存在两种参数包:模板参数包(template parameter packet),表示0个或多个模板参数;函数参数包(function parameter packet),表示0个或多个函数参数。
采用省略号(…)的形式来指出这是一个参数包。
在模板参数列表中,class…或typename…指出接下来的参数表示0个或多个类型的列表。一个类型名后面跟省略号(T…)表示0个或多个给定类型T的非类型参数的列表。在函数参数列表中,如果一个参数的类型是一个模板参数包,则此参数也是一个函数参数包。
eg:
//Args 是一个模板参数包;rest是一个函数参数包
//Args 表示0个或多个模板参数类型
//rest 表示0个或多个函数参数
template<typename T,typename... Args>
void foo(const T &t, const Args& ... rest);
声明了foo函数是一个可变参数函数模板,它有一个名为T的类型参数和一个名为Args的模板参数包。
template<typename T,typename... Args>
void foo(const T &t, const Args& ... rest)
{
cout << sizeof...(Args) << ends;
cout << sizeof...(rest) << ends;
}
void test01()
{
int i = 10;
double d = 3.14;
string s = "hello";
foo(i, s, 42, d);//包中有3个参数
foo("hi");//包中有0个参数
}
void foo(const int&,const string&,const int&,const double&);
void foo(const char[3]&);
每个实例的T类型都是第一个实参的类型,剩下的是参数包。
sizeof...
运算符可以用来查看包中元素个数。
通过递归调用实现可变参数函数print.
template<typename T>
ostream& print(ostream &os,const T &t)
{
return os << t;
}
template<typename T, typename... Args>
ostream& print(ostream &os, const T &t, const Args&... rest)
{
os << t << " ";
return print(os, rest...);
}
void test02()
{
int i = 0;
double d = 3.14;
string s = "hello";
print(cout,i,s,42,d,'\n');
}
结果:
第一个版本的print是一个普通的函数模板,它接受一个输出流和一个T类型,其的功能是输出T,并返回输出流的引用。其负责终止递归并打印初始调用中的最后一个实参。
第二个版本是一个可变参数函数模板,它打印绑定到t的实参,并调用自身打印参数包中剩下的值。
在每一次调用
return print(os, rest...);
时,rest中的第一个包中的实参被绑定到t,剩下的形成下一个print调用的参数包。
那么,在调用
print(cout,i,s,42,d,'\n');
时,会发生如下递归调用:
调用 | t | rest… |
---|---|---|
print(cout,i,s,42,d,’\n’) | i | s,42,d,’\n’ |
print(cout,s,42,d,’\n’) | s | 42,d,’\n’ |
print(cout,42,d,’\n’) | 42 | d,’\n’ |
print(cout,d,’\n’) | d | ‘\n’ |
print(cout,’\n’) 调用普通版本 |
对于最后一次调用,编译器会选择更加特化的普通版本。
注意:普通版本和可变参数版本要在同一作用域,否则会无限递归。
对于一个参数包,除了获取其大小以外,唯一能对其进行的操作就是展开(expand)它。当展开一个包时,我们要提供用于每个扩展元素的模式(pattern)。展开一个包就是将它分解成构成的元素,对每个元素应用模式,获得展开后的列表。通过在模式右边放一个省略号(…)来触发展开操作。
eg:
template<typename T, typename... Args> //展开Args
ostream& print(ostream &os, const T &t, const Args&... rest) //展开rest
第一个展开操作展开模板参数包,为print生成函数参数列表。第二个扩展操作出现在对print的调用中。此模式为print调用生成实参列表。
对Args的展开中,编译器将模式const Args& 应用到模板参数包Args中的每个元素。因此,此模式的扩展结果是一个逗号分隔的0个或多个类型的列表,每个类型形如const type& 。
例如:
print(cout,i,s,42,d,'\n'); //参数包大小为4
最后4个实参的类型和模式一起确定了未知参数的类型。此调用被实例化为:
ostream& print(ostream &, const int&, const string&, const int&, const double&, const char&);
第二个扩展发生在对print的递归调用中。在此情况下,模式是函数参数包的名字(即rest)。此模式扩展出一个由包中元素组成的、逗号分隔的列表。因此,这个调用等价于
print(os,s,42,d,'\n');
使用较为复杂的扩展模式。
//... print 的两个版本
template<typename T>
string ossPrint(const T &t)
{
ostringstream oss;
oss << t;
return oss.str();
};
template<typename... Args>
ostream &msg(ostream &os, const Args&... rest)
{
return print(os, ossPrint(rest)...);
}
void test03()
{
int i = 0;
double d = 3.14;
string s = "hello";
msg(cout, i, s, 42, d, '\n');
}
此处使用的模式是ossPrint(rest),这个模式表示对包中的每一个元素调用ossPrint。
注意省略号的位置。是在ossPrint(rest)…而不是ossPrint(rest…),其原因是ossPrint并不接受一个可变参数包,它只会接受一个普通类型参数。
《C++ Primer 第5版》