可变参数模板
一个可变参数模板就是一个接受可变数目参数的模板函数或模板类。
可变数目的参数被称为参数包。
存在两种参数包:
- 模板参数类,表示零个或多个模板参数;
- 函数参数包,表示零个或多个函数参数。
我们用一个省略号来指出一个模板参数或函数参数表示一个包。
在一个模板参数列表中,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(constdouble&,const string&);
void foo(const char[3]&);
在每个实例中,T的类型都是从第一个实参的类型推断出来的。
剩下的实参(如果有的话)提供函数额外实参的数目和类型。
sizeof...运算符
当我们需要知道包中有多少元素时,可以使用 sizeof...运算符。
类似 sizeof,sizeof...也返回一个常量表达式,而且不会对其实参求值:
template<typename ... Args>
void g(Args ... args)
{
cout << sizeof...(Args) <<end1;//类型参数的数目
cout << sizeof...(args) << endl;// 函数参数的数目
)
sizeof...
是C++11中引入的可变参数的运算符,用于计算变参模板的参数数量。以下是使用sizeof...
运算符的示例:
#include <iostream>
template<typename... Args>
void printArgsSize(Args... args) {
std::cout << "Number of arguments: " << sizeof...(args) << std::endl;
}
int main() {
printArgsSize(); // 输出:Number of arguments: 0
printArgsSize(1, "hello", 3.14, true); // 输出:Number of arguments: 4
return 0;
}
在这个例子中,我们定义了一个可变参数模板函数printArgsSize
,它接受不定数量的参数,并使用sizeof...
运算符来计算参数的数量。在main
函数中,我们调用了printArgsSize
函数两次,分别传入不同数量的参数,并输出参数的数量。
编写可变参数函数模板
我们可以使用一个initializer_list 来定义一个 可接受可变数目实参的函数。
但是,所有实参必须具有相同的类型(或它们的类型可以转换为同一个公共类型)。
当我们既不知道想要处理的实参的数目也不知道它们的类型时可变参数函数是很有用的。
作为一个例子,我们将定义一个函数,它名为error_msg 函数,函数实参的类型是可变的。
我们首先定义一个名为print的函数,它在一个给定流上打印给定实参列表的内容。
可变参数函数通常是递归的。第一步调用处理包中的第一个实参,然后用剩余实参调用自身。
我们的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(cout, i, s, 42);//包中有两个参数
调用 | t | rest... |
print(cout,i,s,42) | i | s,42 |
print(cout,s,42) | s | 42 |
print(cout,42)调用非可变参数模板 |
前两个调用只能与可变参数版本的print匹配,非可变参数版本是不可行的,因为这两个调用分别传递四个和三个实参,而非可变参数print只接受两个实参。
对于最后一次递归调用print(cout,42),两个print版本都是可行的,这个调用传递两个实参,第一个实参的类型为ostream&,因此,可变参数版本的print可以实例化为只接受两个参数:一个是ostream&参数,另一个是const T&参数。
对于最后一个调用,两个函数提供同样好的匹配,但是,非可变参数模板比可变参数更特例化,
因此编译器选择非可变参数版本。
当定义可变参数版本的print时,非可变参数版本的声明必须在作用城中。否则,可变参数版本会无限递归
包扩展
对于一个参数包,除了获取其大小外,我们能对它做的唯一的事情就是扩展它。
当扩展一个包时,我们还要提供用于每个扩展元素的模式。
扩展一个包就是将它分解为构成的元素,对每个元素应用模式,获得扩展后的列表。
我们通过在模式右边放一个省略号(...)来触发扩展操作。
例如,我们的print函数包含两个扩展:
template < typename T, typename... Args>
// 扩展Args
ostream& print(ostream& os, const T& t, const Args&... rest)
{
os << t << ",";
return print(os, rest..); // 扩展 rest
}
第一个扩展操作扩展模板参数包,为print 生成函数参数列表。
第二个扩展操作出现在对print的调用中。此模式为print调用生成实参列表。
对Args的扩展中,编译器将模式const Arg&应用到模板参数包Args中的每个元素。
因此,此模式的扩展结果是一个逗号分隔的零个或多个类型的列表,每个类型都形如const type&。
例如:
print (cout, i, 42);//包中有两个参数
最后两个实参的类型和模式一起确定了尾置参数的类型。此调用被实例化为:
ostream& print (ostreams, const int&, const string&, const int&);
第二个扩展发生在对print的(递归)调用中。
在此情况下,模式是函数参数包的名字(即rest)。此模式扩展出一个由包中元素组成的、逗号分隔的列表。
因此,这个调用等价于:
print(os, s, 42);
理解包扩展
print中的函数参数包扩展仅仅将包扩展为其构成元素,C++语言还允许更复杂的展模式。
例如,我们可以编写第二个可变参数函数,对其每个实参调用 debug_rep,然后调用print打印结果string:
//在print 调用中对每个实参调用debug_rep
template <typename... Args>
ostream& errorMsg (ostream &os, const Args&... rest)
{
// print (os, debug rep(al), debug rep(a2),.... debug_rep(an)
return print(os, debug_rep(rest)...);
}
这个print调用使用了模式debug reg(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(al, a2,..,an))
print(os,debug_rep(rest...));// 错误:此调用无匹配函数
这段代码的问题是我们在debug rep 调用中扩展了rest,它等价于
print (cerr, debug rep(fonName, code.num(),
otherData, "otherData", item));
在这个扩展中,我们试图用一个五个实参的列表来调用debug_rep,但并不存在与此调用匹配的debuq_rep版本。debug_rep函数不是可变参数的,而且没有哪个debug_rep版本接受五个参数。
扩展中的模式会独立地应用于包中的每个元素。
转发参数包
在新标准下,我们可以组合使用可变参数模板与forward机制来编写函数,实现将其实参不变地传递给其他函数。
作为例子,我们将为 StrVec类添加一个emplace_back成员。
标准库容器的 emplace_back成员是一个可变参数成员模板,它用其实参在容器管理的内存空间中直接构造个元素。
我们为Strvec 设计的emplace_back版本也应该是可变参数的,因为string有多个构造函数,参数各不相同。由于我们希望能使用string的移动构造函数,因此还需要保持传递给emplace_back的实参的所有类型信息。
如我们所见,保持类型信息是一个两阶段的过程。
首先,为了保持实参中的类型信息,必须将emplace_back的函数参数定义为模板类型参数的右值引用:
class StrVec
{
public:
template <class... Args> void emplace back (Args&&...);
//....
pair<string*, string*> strvec::alloc_n_copy (const string *b, const string *e)
{
// 分配空间保存给定范围中的元素
auto data = alloc.allocate(e - b);
// 初始化并返回一个pair,该pair由data和uninitialized_copy的返回值构成
return {data, uninitialized_copy(b,e, data)};
}
};
模板参数包扩展中的模式是&&,意味着每个函数参数将是一个指向其对应实参的右值引用。
其次,当emplace back将这些实参传递给construct时,我们必须使用forward来保持实参的原始类型:
template <class... Args>
inline void StrVec::emplace_back (Args&&... args)
{
chk_n_alloc();// 如果需要的话重新分配StrVec 内存空间
alloc.construct(first free++, std::forward<Args>(args)...);
}
emplace_back的函数体调用了chk_n_alloc来确保有足够的空间容纳一个新元素,然后调用了construct在first_free指向的位置中创建了一个元素。
construct调用中的扩展为
std::forward<Args>(args)...
它既扩展了模板参数包Args,也扩展了函数参数包args。此模式生成如下形式的元素
std::forward<Ti>(ti)
其中T表示模板参数包中第1个元素的类型,t,表示函数参数包中第1个元素。例如,假定svec 是一个StrVec,如果我们调用
Svec.emplace_back(10,'c');// 将cceccccecc添加为新的尾元素
construct调用中的模式会扩展出
std::forward<int>(10), std::forward<char>(c)
通过在此调用中使用 forward,我们保证如果用一个右值调用emplace_back,则construct也会得到一个右值。
例如,在下面的调用中:
svec.emplace_back(s1 + s2);//使用移动构造函数
传递给emplace back的实参是一个右值,它将以如下形式传递给construct
std::forward<string>(string("the end"))
forward<string>的结果类型是string&&,因此construct将得到一个右值引用实参。
construct会继续将此实参传递给string的移动构造函数来创建新元素。
建议:转发和可变参数模板
可变参数函数通常将它们的参数转发给其他函数。这种函数通常具有与我们的emplace back函数一样的形式:
// fun有零个或多个参数,每个参数都是一个模板参数类型的右值引用 template<typename... Args> void fun(Args&&... args) //将Args扩展为一个右值引用的列表 { // work 的实参既扩展 Args又扩展 args work(std::forward<Args>(args)...); }
这里我们希望将fun的所有实参转发给另一个名为work的函数,假定由它完成函数的实际工作。类似emplace back中对construct的调用,work调用中的扩展既扩展了模板参数包也扩展了函数参数包。由于 fun 的参数是右值引用,因此我们可以传递给它任意类型的实参;由于我们使用std::forward传递这些实参,因此它们的所有类型信息在调用work时都会得到保持。