这一节主要看一个比较晦涩的特性,变长模板。
C++11标准库中提供了一个tuple模板,通过它我们可以声明任意多个不同类型的元素的集合。例如
std::tuple<int, char, std::string> collections;
该collections变量可以容纳int, char, std::string三种类型的数据。当然我们也可以更为简单的利用C++11的make_tuple模板函数来创造一个tuple模板类型。这里学习一下tuple的实现方式。
1. 变长模板的语法,模板参数包和函数参数包
template<typename ... Elements> class tuple;
可以看到,我们在标识符Elements之前使用了省略号(...)来表示该参数是变长的。在C++11中,Elements被称为是一个“模板参数包(template parameter pack)”。
一个模板参数包在模板推导时会被认为是模板的单个参数(虽然它可以打包任意数量的实参)。为了使用模板参数包,我们需要使用一个名为“包拓展”的表达式来完成解包。例如:
template<typename T1, template T2> class B{};
template<typename ... A> class TemplateClass : private B<A ... >{};
这里我们为类模板TemplateClass声明了一个参数包A,而使用该参数包A...则是在TemplateClass的私有基类B<A...>中。当然,这里的疑问是B模板总是接受两个参数,倘若我们声明TemplateClass<X, Y, Z>必定会导致模板推导的错误。那么如何才能使模板能够接受任意多个的模板参数并均能实例化呢?
实际上,在C++11中,实例化tuple模板的方式就给出了一种使用模板参数包的答案。其思路是使用数学归纳法,转化为计算机能够实现的手段则是递归。
简化的tuple实现大致类似这样:
template<typename ... Elements> class tuple; template<typename Head, typename ... Tail> class tuple<Head, Tail...> : private tuple<Tail ...>{//递归的偏特化定义 Head head; }; template<> class tuple<> {};//边界条件
设想我们实例化一个形如tuple<double, int, char>的类型时,会引起基类的递归构造,这样的递归直到tuple的参数包为0个的时候会结束。还原过程,编译期将从tuple<>出发,构造出tuple<char>,继而构造出tuple<int, char>,tuple<double, int, char>类型。
当然了,变长的函数参数也可以声明为函数参数包(function parameter pack)。例如:
template<typename ... T> void func(T ... args);
C++11标准要求函数参数包必须唯一 且是函数的最后一个参数。可以看一下这个Printf的例子:
void Printf(const char* s) { while (*s) { if (*s == '%' && *++s != '%') { throw runtime_error("invalid format string: missing arguments"); } cout << *s++; } } template<typename T, typename ...Args> void Printf(const char *s, T value, Args...args) { while (*s) { if (*s == '%' && *++s != '%') { cout << value; return Printf(++s, args...); } cout << *s++; } throw runtime_error("extra arguments provided to Printf"); }
2. 进阶
在上面TemplateClass的例子中,我们看见参数包在类的基类描述列表中进行了展开。而按照C++11的标准,参数包可以在下面7个位置进行展开:
表达式 |
初始化列表,使用{}进行列表初始化的地方 |
基类描述列表 |
类成员初始化列表 |
模板参数列表 |
通用属性列表 |
lambda函数的捕捉列表 |
对于包拓展而言,其解包与其声明的形式息息相关。比如声明了Arg为参数包,那么我们可以使用Arg&& ... 这样的包拓展表达式,其解包后等价于Arg1&&, Arg2&&, ... , ArgN&&(设Arg1是参数包的第一个参数而ArgN是最后一个)。
再看这个例子:
template<typename ... A> class T : privateB<A> ... {}; template<typename ... A> class T : private B<A ... > {};
倘若我们实例化T<X, Y>,对于前者,会被解包为class T<X, Y> : private B<X>, private B<Y> {}; 而后者会被解包为class T<X, Y> : private B<X, Y>{}。
类似的情况也会发生在函数声明上面。
template<typename ... T> void DummyWrapper(T...t) {} template<typename T> T print(T t) { cout << t; return t; } template<typename ... T> void Print(T...t) { cout << sizeof...(t) << endl; DummyWrapper(print(t)...); }
这个例子中DummyWrapper(print(t)...)的包拓展会被解包为print(Arg1),print(Arg2),... ,print(ArgN)。但如果我们测试Print("a", "b", "c"),我用g++编译得到的输出结果并非a, b, c,而是倒序的c, b, a。
可以使用sizeof...操作符来计算参数包中的参数个数。
最后是一个与完美转发结合的例子,Build函数看起来复杂,理解其原理还是很好读懂的。
struct A { A() {} A(const A& a) { cout << "A Copy Constructed " << __func__ << endl; } A(A && a) { cout << "A Move Constructed " << __func__ << endl; } void Func() { cout << "A" << endl; } }; struct B { B() {} B(const B& b) { cout << "B Copy Constructed " << __func__ << endl; } B(B && b) { cout << "B Move Constructed " << __func__ << endl; } void Func() { cout << "B" << endl; } }; template<typename ... T> struct MultyTypes;//声明 template<typename First, typename ...T> struct MultyTypes<First, T...> : public MultyTypes<T...> {//递归偏特化 First first; MultyTypes<First, T...>(First f, T...t) : first(f), MultyTypes<T...>(t...) {} }; template<> struct MultyTypes<> {};//边界条件 template<template <typename ...> class VariadicType, typename ...Args> VariadicType<Args...> Build(Args&& ... args) {//转发 return VariadicType<Args...>(std::forward<Args>(args)...); } int main() { A a; B b; Build<MultyTypes>(b, a);//在构造的时候不会调用任何的拷贝构造函数或者移动构造函数,\ 构造之后的类型只包含了对之前定义的变量a和b的引用 system("pause"); return 0; }