可变参数模板
一个可变参数模板就是一个接收可变数目参数的模板函数或模板类。可变数目的参数被称为参数包。存在两种参数包:模板参数包,表示零个或多个模板参数;函数参数包,表示零个或多个函数参数。
使用一个省略号来指出一个模板参数或函数参数表示一个包。在一个模板参数列表中,class...
或typename...
指出接下来的参数表示零个或多个类型的列表;一个类型名后面跟一个省略号表示零个或多个给定类型的非类型参数的列表。在函数参数列表中,如果一个参数的类型是一个模板参数包,则此参数也是一个函数参数包。
例如
//Args是一个模板参数包;rest是一个函数参数包
//Args表示零个或多个模板类型参数
//rest表示零个或多个函数参数
template<typename T, typename... Args>
void foo(const T& t, const Args&... rest);
声明了foo
是一个可变参数函数模板,他有一个名为T
的类型参数,和一个名为Args
的模板参数包。这个包表示零个或多个额外的类型参数。foo
的函数参数列表包含一个const&
类型的参数,指向T
的类型,还包含一个名为rest
的函数参数包,此包表示零个或多个函数参数。
与往常一样,编译器从函数的实参推断模板参数类型。对于一个可变参数模板,编译器还会推断包中参数的数目。例如,给定下面的调用
int i = 0;double d = 3.14;string s = "how now brown cow";
foo(i, s, 42, d); //包中有三个参数
foo(s, 42,"hi"); //包中有两个参数
foo(d, s); //包中有一个参数
foo("hi"); //空包
编译器会为foo
实例化出四个不同的版本
void foo(const int&,const string&, const int&, const double&);
void foo(const string&, const int&, const char[3]&);
void foo(const double&, const string&);
void foo(const char[3]&);
sizeof…运算符
当我们需要知道包中有多少个元素时,可以使用sizeof...
运算符。sizeof...
返回一个常量表达式,而且不会对其实参求值。
template<typename... Args>
void g(Args... args)
{
cout << sizeof...(Args) << endl; //类型参数的数目
cout << sizeof...(args) << endl; //函数参数的数目
}
编写可变参数函数模板
可以使用一个initializer_list
定义一个可接受可变数目实参的函数。但是,所有实参必须具有相同的类型(或它们的类型可以转换为同一个公共类型)。当我们既不知道想要处理的实参的数目也不知道它们的类型时,可变参数函数是很有用的。
可变参数函数通常是递归的。第一步调用处理包中的第一个实参,然后用剩余实参调用自身。print
函数是这样的模式,每次递归调用将第二个实参打印到第一个实参表示的流中。为了终止递归,还需要定义一个非可变参数的print
函数,它接收一个流和一个对象:
//用来终止递归并打印最后一个元素的函数
//此函数必须在可变参数版本的print定义之前声明
template<typename T>
ostream &print(ostream& os, const T& t)
{
return os << t;
}
//包中除了最后一个元素之外的其他元素都会调用这个版本的print
template<typename T,typename... Args>
ostream& print(ostream& os, const T& t, const Args&... rest)
{
os << t << ", "; //打印第一个实参
return print(os, rest...); //递归调用,打印其他实参
}
第一个版本的print
负责终止递归并打印初始调用中的最后一个实参。第二个版本的print
是可变参数版本,它打印绑定到t
的实参,并调用自身来打印函数参数包中的剩余值。
这段程序的关键部分是可变参数函数中对print
的调用:
return print(os, rest...); //递归调用,打印其他实参
我们的可变参数版本的print
函数接受三个参数:一个是ostream&
,一个const T&
和一个参数包。而此调用值传递了两个实参。其结果是rest
中的第一个实参被绑定到t
,剩余实参形成下一个print
调用的参数包。因此,在每个调用,包中的第一个实参被移除,称为绑定到t
的实参。即,给定
print(count, i, s, 42); //包中有两个参数
递归会执行如下:
调用 | t | rest… |
---|---|---|
print(count,i,s,42) | i | s,42 |
print(count,s,42) | s | 42 |
print(count,42)调用非可变参数版本的print |
前两个调用只能与可变参数版本的print
匹配,非可变参数版本不可行,因为这两个调用分别传递四个和三个实参,而非可变参数print
只接受两个实参。
对于最后一次递归调用print(cout, 42)
,两个print
版本都是可行的。但是,非可变参数模板比可变参数模板更特例化,因此编译器选择非可变参数版本。
包扩展
扩展一个包就是将它分解为构成的元素,对每个元素应用模式,获得扩展后的列表。我们通过在模式右边放一个省略号(...
)来触发扩展操作。
例如,print
函数包含两个扩展:
template<typename T, typename... Args>
ostream& print(ostream& os, const T& t, const Args&... rest) //扩展Args
{
os << t << ", ";
return print(os, rest...);
}
第一个扩展操作扩展模板参数包,为print
生成函数参数列表。第二个扩展操作出现在对print
调用。此模式为print
调用生成实参列表。
对Args
的扩展中,编译器将模式const Args&
应用到模板参数包Args
中的每个元素,因此,此模式的扩展结果是一个逗号分割的零个或多个类型的列表,每个类型都形如const type&
,例如
print(count,i,s,42); //包中有两个参数
最后两个实参的类型和模式一起确定了尾置参数的类型。此调用被实例化为
ostream& print(ostream&, const int&,const string&,const int&);
第二个扩展发生在对print
的递归调用。在此情况下,模式是函数参数包的名字()
理解包扩展
可以编写第二个可变参数函数,对其每个实参调用debug_rep
,然后调用print
打印结果string
。
//在print调用中对每个实参调用debug_rep
template<typename... Args>
ostream& errorMsg(ostream& os, const Args&... rest)
{
//print(os, debug_rep(a1), debug_rep(a2), ..., debug_rep(an))
return print(os, debug_rep(rest)...);
}
这个print
调用使用了模式debug_rep(rest)
。此模式表示我们希望对函数参数包rest
中的每个元素调用debug_rep
。扩展结果将是一个逗号分割的debug_rep
调用列表。即,下面调用:
errorMsg(cerr, fcnName, code.num(), otherData, "other", item);
print(cerr, debug_rep(fcnName), debug_rep(code.num())
debug_rep(otherData), debug_rep("otherData").
debug_rep(item));
与之相对,下面的模式会编译失败
//将包传递给debug_rep;print(os, debug_rep(a1,a2,...,an))
print(os,debug_rep(rest...))//错误,此调用无匹配函数
这段代码的问题是我们在debug_rep
调用中扩展了rest
。它等价于
print(cerr, debug_rep(fcnName, code.num(), otherData, "other", item));
扩展中的模式会独立地应用于包中的每个元素
转发参数包
可以组合使用可变参数模板与forward
机制来编写函数,实现将实参不变地传递给其他函数。
保持类型信息是一个两阶段的过程。首先,为了保持实参中的类型信息,必须将emplace_back
的函数参数定义为模板类型参数的右值引用。
class StrVec{
public:
template<class... Args>
void emplace_back(Args&&...);
}
模板参数包扩展中的模式是&&
,意味着每个函数参数将是一个指向其对应实参的右值引用。
其次,当emplace_back
将这些实参传递给construct
时,必须使用forward
来保持实参的原始类型。
template<class... Args>
inline void StrVec::emplace_back(Args&&... args)
{
chk_n_alloc();
alloc.construct(first_free++, std::forward<Args>(args)...);
}