引入
可变模板是C++11中非常重要也是非常值得探讨的一个语法。首先它是一个模板,可以是函数模板,也可以是类模板;其次可变也有两层含义,一方面是指模板参数的个数可变,另一方面是指模板参数的类型可变。
实现可变模板的关键字说起来非常简单,就是三个点(即…,我在后面将其统一称为可变模板关键字),和汉语中的省略号很相似,都有数量不定的意思,而且可变模板本身其实也就只有这一个意思,只是用户通过递归的技巧可以设计出让人惊诧的应用。
可变模板用于函数模板
利用模板参数逐一递减的特性,实现递归函数调用,最终的目的是实现一组任意数量任意类型数据的逐一处理。形式如下:
void func()
{
//递归出口
}
template<typename T,typename... Types>//①...表明模板参数Types的个数和类型都是不定的
void func(const T& firstArg,const Types&... args)//②...表明函数参数包args的个数是不定的
{
//处理数据firstArg
func(args...);//③...表明函数参数args的个数是不定的
//递归调用
}
注意上面的代码中可变模板关键字的三种使用形式,还需要注意可变模板用于模板函数时都会有一个递归出口函数,这个出口函数与模板函数同名,但是没有参数。
例1:打印一组数
......
void print()
{
//递归出口
cout<<"输出完毕"<<endl;
}
template<typename T,typename... Types>//①...表明模板参数Types的个数和类型都是不定的
void print(const T& firstArg,const Types&... args)//②...表明函数参数包args的个数是不定的
{
//sizeof...(args)用于获取参数包中的参数个数
static int args_count=sizeof...(args);//注意这里的静态static!!!!!!!!!!!
//处理数据firstArg
cout<<"倒数第"<<args_count+1<<"个元素为:"<<firstArg<<endl;
print(args...);//③...表明函数参数args的个数是不定的
//递归调用
}
......
//执行调用
print(1,2.3,"hello",'c');
上面的例子很简单,输入一包类型各不相同的数据,但后打印出来。通过static int args_count=sizeof…(args) 我们需要关注的有两点:
第一点最浅显的就是参数包 args中的参数个数在逐步减一,这是由于print(args…) 执行的递归调用,每次会从参数包args中分离出一个参数firstArg和一个子参数包args,最终当参数包中的参数个数为0时,会调用递归函数出口print();
第二点较为深入,我们使用了静态局部变量args_count,对于普通函数而言,静态局部变量只有第一次调用函数时完成定义初始化并保存在内存的全局变量区,后续再次调用函数时改变量可以直接承接上次调用继续使用(这就是我们所说的静态局部变量具有记忆性或累积性),小demo如下:
template<typename T>
void test1(T a)
{
static int b = a;
cout << b << endl;
if (a == 2)return;
test1(2);
}
......
test1(1);
然而我们在该模板函数中使用静态局部变量,发现每次args_count的值都不一样,因此可以大胆推测,对于存在可变模板的模板函数,每次递归调用编译器都会重载并调用一个新的函数,这个新函数与上一层函数相比参数个数少了一个。
根据上面的例子我们再来探讨一个有趣的小问题,以下两个带可变模板的模板函数可否共存呢:
......
//①
template<typename T,typename... Types>
void print(const T& firstArg,const Types&... args){/*......*/}
......
//②
template<typename... Types>
void print(const Types&... args){/*......*/}
理论上,由于①和②都可以接受任意个数任意相同或不同类型的参数,两个函数放在一起当执行**print(1,2.3,“hello”,‘c’)**应该会报有歧义的错误。然而并非如此,因为②可以看作是①的泛化,亦即①为②的特化,模板的特化概念指明面对两者皆可的情况谁更特化编译器就调用谁,因此这里编译器会调用特化版本①。当然虽然这里两个模板函数可以并存,但由于①的存在②永远不可能被调用。
例2:一组数比大小
实际上对于一组数,若类型相同,找出这组数中的最大值大可不必使用可变模板,直接使用初始化列表**initializer_list<>**即可。众所周知,C++2.0之前,标准库仅仅提供了两个数比大小的函数:
......
cout << max(1, 2) << endl;
......
C++2.0之后提供了初始化列表,可以同时对一组数据比大小,但需要把这一组数据用{}包起来(这里需要小小的说明以下,VS2015默认支持C++11的部分功能,VS2017才完整支持C++11,以下这句代码就是在VS2017下才编译通过的):
......
cout << max({1,2,3,4,5}) << endl;
......
这里借用可变模板同样可以实现一组数据比大小:
......
int maxNum(const int& n)
{
return n;
}
template<typename... T>
int maxNum(int firstArg, const T&... args)
{
return max(firstArg, maxNum(args...));
}
......
cout << maxNum(1, 2, 3, 4, 5) << endl;
可变模板用于类模板
利用模板参数个数逐一递减导致模板类型也逐一递减的特性,实现递归继承或递归复合。递归调用处理的是函数的参数,使用函数模板,而递归的继承和复合处理的都是类型,使用的是类模板。
例3:用于递归继承
......
template<typename... Types> class Tup;
template<> class Tup<> {};
template<typename Head,typename... Tail>
class Tup<Head, Tail...> :public Tup<Tail...>{
typedef Tup<Tail...> Base;
protected:
Head m_head;
public:
Tup() {}
Tup(Head h,Tail... t):m_head(h),Base(t...){}
Head& head() { return m_head; }
Base& tail() { return *this; }//这里用到了一个很有意思的转型技巧,*this相当于子类对象,返回值类型Base为父类类型,子类转父类(向上类型转换)会出现成员收缩。即类型转换为父类对象后,父类对象不再具备访问原有子类对象新成员的权力,而只能访问父类本身已有的成员。在这里返回对象的指针相当于指向了头部元素的下一个元素
};
......
Tupl<int, string, double> tupl(1, "234", 5.0);
cout << tupl.head() << " " << tupl.tail().head() << " " << tupl.tail().tail().head() << endl;
例4:用于递归复合
......
template<typename...Types> class Tup;//类型声明
class Tup<> {};//递归出口
template<typename Head,typename... Tail>
class Tup<Head, Tail...> {//模板参数在这里分开1+n
typedef Tup<Tail...> Composit;
protected:
Head m_head;
Composit m_tail;
public:
Tup() {}
Tup(Head h,Tail...t):m_head(h),m_tail(t...){}
Head& head() { return m_head; }//这里返回引用用户才能在外界修改容器内的值
Composit& tail() { return m_tail; }//这里返回引用用户才能在外界修改容器内的值
//如何实现获取容器中的任意位置的元素(待完成)
};
......
Tup<int, string, double> tup(1, "234", 5.0);
cout<<tup.head()<<" "<<tup.tail().head()<<" "<<tup.tail().tail().head()<< endl;
tup.head() = 2;
tup.tail().head() = "456";
tup.tail().tail().head() = 6.0;
cout<<tup.head()<<" "<<tup.tail().head()<<" "<<tup.tail().tail().head()<< endl;