可变参数模板
一个可变参数模板就是接受一个可变数目参数的模板函数或模板类。可变数目的参数被称为参数包。存在两种参数包:模板参数包,表示零个或多个模板参数;函数参数包,表示零个或多个函数参数。
我们用一个省略号来指出一个模板参数或函数参数表示一个包。在一个模板参数列表中,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]&);
在每个实例中,T 的类型都是从第一个实参的类型推断出来的。剩下的实参提供函数额外实参的数目和类型。
sizeof… 运算符
当我们需要知道包中有多少元素时,可以用 sizeof… 运算符。sizeof… 返回一个常量表达式,而且不会对其实参求值:
template <typename T,typename... Args> void f(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 的调用:
return print(os, rest...);
我们可以发现,可变参数版本的 print 有三个参数,os,const T& 与 一个参数包。而此调用只传递了两个实参。其结果是 rest 中的第一个实参被绑定到 t,剩余实参形成下一个 print 调用的参数包。当此包中只剩下一个参数时,虽然两个版本的 print 都能够精确匹配,但是非函数模板优先于函数模板,所以最后一个 print 调用的非函数模板的 print。
当定义可变参数版本的 print 时,非可变参数版本的声明必须在作用域中。否则,可变参数版本可能会无限递归。
包扩展
对于一个参数包,除了获取其大小外,我们对它做的唯一的事情就是扩展它。当扩展一个包时,我们还要提供用于每个扩展元素的模式。扩展一个包就是将它分解为构成的元素,对每个元素应用模式,获得扩展后的列表。我们通过在模式右边放一个省略号(…) 来触发扩展操作。
例如,我们的 print 函数包含两个扩展:
template <typename T,typename... Args>
ostream& print(ostream &os,const T &t,const Args&... rest) { // 扩展 Args
os << t << ",";
return print(os,rest...); // 扩展 rest
}
理解包扩展
print 中的函数参数包扩展仅仅将包扩展为其构成元素,C++还允许更为复杂的扩展模式。例如,我们可以编写第二个可变参数函数,对其每个实参调用 debug_rep,然后调用 print 打印结果 string:
template <typename T> string debug_rep(const T&t) {
ostringstream ret;
ret << t;
return ret.str();
}
template <typename T> string debug_rep(T *p) {
ostringstream ret;
ret << "pointer: " << p;
if(p) ret << " " << debug_rep(*p); // 打印 p 指向的值
else ret << " null pointer";
return ret.str();
}
string debug_rep(const string &s) { return '"' + s + '"'; }
string debug_rep(char *p) { return debug_rep(string(p)); }
string debug_rep(const char *p) { return debug_rep(string(p)); }
// 以上是 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_reg(rest)。此模式表示我们希望对函数参数包 rest 中的每个元素调用 debug_rep。扩展结果将是一个逗号分隔的 debug_rep 调用列表。即,下面调用:
errorMsg(cerr,fcnName,code.num(),otherDate,"other",item);
就好像我们这样编写代码一样:
print(cerr,debug_rep(fcnName),debug_rep(code.num()),debug_rep(otherData),
debug_rep("other"),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));
在这个扩展中,我们试图用一个五个实参的列表来调用 debug_rep,但并不存在与此调用匹配的 debug_rep 版本。
转发参数包 (emplace_back实现)
在新标准下,我们可以组合使用可变参数模板与 forward 机制来编写函数,实现将其实参不变地传递给其他函数。作为例子,我们将为 StrVec 类添加一个 emplace_back 成员。标准库容器的 emplace_back 成员是一个可变参数成员模板,它用其实参管理的内存空间中直接构造一个元素。
// 这里的 StrVec类省略部分成员,只提供 emplace_back 所需求的成员
class StrVec {
public:
// 省略
private:
static std::allocator<std::string> alloc;
std::pair<std::string*, std::string*> alloc_n_copy
(const std::string*, const std::string*);
void chk_n_alloc(); // 保证内存能够存储元素
std::string *elements; // 指向分配的内存中的首元素
std::string *first_free; // 指向最后一个实际元素之后的位置
std::string *cap; // 指向分配的内存末尾之后的位置
};
我们为 StrVec 设计的 emplace_back 版本也应该是可变参数的,因为 string 有多个构造函数,参数各不相同。由于我们希望能使用 string 的移动构造函数,因此还需要保存传递给 emplace_back 的实参的所有类型信息。
如我们所知道的,保持类型信息是一个两阶段的过程。首先,为了保持实参中的类型信息,必须将 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(); // 如果需要的话重新分配 StrVec 的内存空间
alloc.construct(first_free++,std::forward<Args>(args)...);
// construct本身也是构造,与 emplace_back 类似
}
我们可以发现,construct 中的扩展为:std::forward<Args>(args)…
它既扩展了模板参数包 Args,也扩展了函数参数包 args。此模式生成如下形式的元素:
std::forward<Ti>(ti);
其中 Ti 表示模板参数包中第 i 个元素的类型,ti 表示函数参数包中第 i 个元素。例如,假定 svec 是一个 StrVec,如果我们调用:svec.emplace_back(10,‘c’);
construct 调用中的模式会扩展出:
std::forward<int>(10),std::forward<char>(c)
通过在此调用中使用 forward,我们保证如果用一个右值引用调用 emplace_back,则 construct 也会得到一个右值 (这是 forward 可以保证的)。