【C++基础】系列博客为参考《C++ Primer中文版(第5版)》(C++11标准)一书,自己所做的读书笔记。
本文为原创文章,未经本人允许,禁止转载。转载请注明出处。
1.可变参数模板
一个可变参数模板(variadic template)就是一个接受可变数目参数的模板函数或模板类。可变数目的参数被称为参数包(parameter packet)。存在两种参数包:模板参数包(template parameter packet),表示零个或多个模板参数;函数参数包(function parameter packet),表示零个或多个函数参数。
我们用一个省略号来指出一个模板参数或函数参数表示一个包。在一个模板参数列表中,class...
或typename...
指出接下来的参数表示零个或多个类型的列表:一个类型名后面跟一个参略号表示零个或多个给定类型的非类型参数的列表。在函数参数列表中,如果一个参数的类型是一个模板参数包,则此参数也是一个函数参数包。例如:
1
2
3
4
5
//Args是一个模板参数包;rest是一个函数参数包
//Args表示零个或多个模板类型参数
//rest表示零个或多个函数参数
template <typename T, typename... Args>
void foo(const T &t, const Args& ... rest);
声明了foo是一个可变参数函数模板,它有一个名为T的类型参数,和一个名为Args的模板参数包。这个包表示零个或多个额外的类型参数。foo的函数参数列表包含一个const &
类型的参数,指向T的类型,还包含一个名为rest的函数参数包,此包表示零个或多个函数参数。
与往常一样,编译器从函数的实参推断模板参数类型。对于一个可变参数模板,编译器还会推断包中参数的数目。例如,给定下面的调用:
1
2
3
4
5
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实例化出四个不同的版本:
1
2
3
4
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的类型都是从第一个实参的类型推断出来的。剩下的实参(如果有的话)提供函数额外实参的数目和类型。
1.1.sizeof...
运算符
当我们需要知道包中有多少元素时,可以使用sizeof...
运算符。类似sizeof,sizeof...
也返回一个常量表达式,而且不会对其实参求值:
1
2
3
4
template<typename ... Args> void g(Args ... args) {
cout << sizeof...(Args) << endl; //类型参数的数目
cout << sizeof...(args) << endl; //函数参数的数目
}
2.编写可变参数函数模板
我们可以使用一个initializer_list
来定义一个可接受可变数目实参的函数。但是,所有实参必须具有相同的类型(或它们的类型可以转换为同一个公共类型)。当我们既不知道想要处理的实参的数目也不知道它们的类型时,可变参数函数是很有用的。作为一个例子,我们将定义一个函数,它类似较早的error_msg函数,差别仅在于新函数实参的类型也是可变的。我们首先定义一个名为print的函数,它在一个给定流上打印给定实参列表的内容。
可变参数函数通常是递归的。第一步调用处理包中的第一个实参,然后用剩余实参调用自身。我们的print函数也是这样的模式,每次递归调用将第二个实参打印到第一个实参表示的流中。为了终止递归,我们还需要定义一个非可变参数的print函数,它接受一个流和一个对象:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
//用来终止递归并打印最后一个元素的函数
//此函数必须在可变参数版本的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的调用:
1
return print(os, rest...); //递归调用,打印其他实参
我们的可变参数版本的print函数接受三个参数:一个ostream&
,一个const T&
和一个参数包。而此调用只传递了两个实参。其结果是rest中的第一个实参被绑定到t,剩余实参形成下一个print调用的参数包。因此,在每个调用中,包中的第一个实参被移除,成为绑定到t的实参。即,给定:
1
print(cout, i, s, 42); //包中有两个参数
递归会执行如下:
对于最后一个调用,两个函数提供同样好的匹配。但是,非可变参数模板比可变参数模板更特例化,因此编译器选择非可变参数版本(参见:重载与模板)。
3.包扩展
对于一个参数包,除了获取其大小外,我们能对它做的唯一的事情就是扩展(expand)它。当扩展一个包时,我们还要提供用于每个扩展元素的模式(pattern)。扩展一个包就是将它分解为构成的元素,对每个元素应用模式,获得扩展后的列表。我们通过在模式右边放一个省略号(...
)来触发扩展操作。
例如,我们的print函数包含两个扩展:
1
2
3
4
5
6
7
template <typename T, typename... Args>
ostream &
print(ostream &os, const T &t, const Args&... rest) //扩展Args
{
os << t << ", ";
return print(os, rest...); //扩展rest
}
第一个扩展操作扩展模板参数包,为print生成函数参数列表。第二个扩展操作出现在对print的调用中。此模式为print调用生成实参列表。
对Args的扩展中,编译器将模式const Arg&
应用到模板参数包Args中的每个元素。因此,此模式的扩展结果是一个逗号分隔的零个或多个类型的列表,每个类型都形如const type&
。例如:
1
print(cout, i, s, 42); //包中有两个参数
最后两个实参的类型和模式一起确定了尾置参数的类型。此调用被实例化为:
1
2
ostream&
print(ostream&, const int&, const string&, const int&);
第二个扩展发生在对print的(递归)调用中。在此情况下,模式是函数参数包的名字(即rest)。此模式扩展出一个由包中元素组成的、逗号分隔的列表。因此,这个调用等价于:
1
print(os, s, 42);
3.1.理解包扩展
print中的函数参数包扩展仅仅将包扩展为其构成元素,C++语言还允许更复杂的扩展模式。例如,我们可以编写第二个可变参数函数,对其每个实参调用debug_rep,然后调用print打印结果string:
1
2
3
4
5
6
7
//在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调用列表。即,下面调用:
1
errorMsg(cerr, fcnName, code.num(), otherData, "other", item);
就好像我们这样编写代码一样:
1
print(cerr, debug_rep(fcnName), debug_rep(code.num()), debug_rep(otherData), debug_rep("otherData"), debug_rep(item));
与之相对,下面的模式会编译失败:
1
2
//将包传递给debug_rep; print(os, debug_rep(a1,a2,...,an))
print(os, debug_rep(rest...)); //错误:此调用无匹配函数
这段代码的问题是我们在debug_rep调用中扩展了rest,它等价于:
1
print(cerr, debug_rep(fcnName, code.num(), otherData, "otherData", item));
在这个扩展中,我们试图用一个五个实参的列表来调用debug_rep,但并不存在与此调用匹配的debug_rep版本。debug_rep函数不是可变参数的,而且没有哪个debug_rep版本接受五个参数。
4.转发参数包
在新标准下,我们可以组合使用可变参数模板与forward机制来编写函数,实现将其实参不变地传递给其他函数。作为例子,我们将为StrVec类添加一个emplace_back成员。标准库容器的emplace_back成员是一个可变参数成员模板,它用其实参在容器管理的内存空间中直接构造一个元素。
我们为StrVec设计的emplace_back版本也应该是可变参数的,因为string有多个构造函数,参数各不相同。由于我们希望能使用string的移动构造函数,因此还需要保持传递给emplace_back的实参的所有类型信息。
如我们所见,保持类型信息是一个两阶段的过程。首先,为了保持实参中的类型信息,必须将emplace_back的函数参数定义为模板类型参数的右值引用(参见:转发):
1
2
3
4
5
class StrVec {
public:
template <class... Args> void emplace_back(Args&&...);
//其他成员的定义和之前一样
};
模板参数包扩展中的模式是&&
,意味着每个函数参数将是一个指向其对应实参的右值引用。
其次,当emplace_back将这些实参传递给construct时,我们必须使用forward来保持实参的原始类型:
1
2
3
4
5
6
7
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调用中的扩展为:
1
std::forward<Args>(args)...
它既扩展了模板参数包Args,也扩展了函数参数包args。此模式生成如下形式的元素:
1
std::forward<Ti>(ti)
其中Ti表示模板参数包中第i个元素的类型,ti表示函数参数包中第i个元素。例如,假定svec是一个StrVec,如果我们调用:
1
svec.emplace_back(10, 'c'); //将cccccccccc添加为新的尾元素
construct调用中的模式会扩展出:
1
std::forward<int>(10), std::forward<char>(c)
通过在此调用中使用forward,我们保证如果用一个右值调用emplace_back,则construct也会得到一个右值。例如,在下面的调用中:
1
svec.emplace_back(s1 + s2); //使用移动构造函数
传递给emplace_back的实参是一个右值,它将以如下形式传递给construct:
1
std::forward<string>(string("the end"))
forward<string>
的结果类型是string&&
,因此construct将得到一个右值引用实参。construct会继续将此实参传递给string的移动构造函数来创建新元素。