可变参类模板也和可变参函数模板一样,允许模板定义含有0到多个(任意个)模板参数。可变参类模板参数包的展开方式有多种,以下介绍几种常见的方法。
一、递归继承展开
1.1类型模板参数包的展开
首先先看下面的代码:
//类型模板展开
//泛化版本
template<typename...Args>
class myclasst {
public:
myclasst() {
std::cout << "myclasst::myclasst()泛化版本执行了,this = " << this
<< " sizeof...(Args) = " << (sizeof...(Args)) << "\n";
}
};
template<typename First, typename... Others>
class myclasst<First, Others...> :private myclasst<Others...> { //继承偏特化版本
public:
First m_i;
myclasst() :m_i(0) {
std::cout << "myclasst::myclasst()偏特化版本执行了,this = " << this
<< " sizeof...(Others) = " << (sizeof...(Others)) << "\n";
}
};
void Test1() {
myclasst<int, float, float>myc;
}
这里,我们定义了一个可变参类模板的泛化版本,通过 s i z e o f . . . sizeof... sizeof...计算出参数数量,我们还打印了 t h i s this this指针。
然后,我们又定义了一个偏特化类型,这个偏特化类型继承了另一个偏特化版本。可以发现,继承的参数列表参数减少了一个。
我们运行后,得到以下结果。
容易发现,
t
h
i
s
this
this指针是一致的,而且打印的参数是从少到多的。
因此,我们可以得到结论:
对于这种继承来展开的参数包,我们是递归到空参,调用泛化版本构造了这样的一个类,作为祖宗类,然后再通过这个祖宗不断通过继承链展开。
由于是继承关系,所以 t h i s this this指针显然是相同的。
相应的关系如下图所示:
当然,如果我们对泛化版本进行一个空参的偏特化,如下:
//使用特殊的特化版本
template<>
class myclasst<> {
public:
myclasst() {
std::cout << "myclasst<>::myclasst()特殊的特化版本执行了,this = " << this << "\n";
}
};
可以发现,这次最开始实例化的模板是这个空参的特化版本,注意这里并不是全特化(可变参模板不存在全特化)
有时,如果我们不需要使用泛化版本(就像上面的样例),直接声明即可:
//直接声明
template<typename...Args>
class myclasst;
1.2非类型模板参数包的展开
参数包不仅可以是类型的,也可以是非类型的。
看下面的范例:
//非类型模板参数的展开
//泛化版本
template<auto... FArgs>
class myclasst2 {
public:
myclasst2() {
std::cout << "myclasst2::myclasst2()偏特化版本执行了,this = " << this
<< " sizeof...(Others) = " << (sizeof...(FArgs)) << "\n";
}
};
//偏特化版本
template<auto First, auto... Others>
class myclasst2<First, Others...> :public myclasst2<Others...> {
public:
myclasst2() {
std::cout << "myclasst2::myclasst2()偏特化版本执行了,this = " << this
<< " sizeof...(Others) = " << (sizeof...(Others)) << ",First = " << First << "\n";
}
};
void Test2() {
myclasst2<1, 2LL, 'a', 4, 5>myc;
}
可以发现,这里的参数列表使用了 a u t o auto auto,这样更灵活一些。实际上,如果想要固定类型,如 ( i n t , c h a r , d o u b l e ) (int,char,double) (int,char,double)等类型,也可以直接使用。
对于非类型参数包的展开,大体上和类型参数包展开一致,这里直接调用,得到:
同样,对于参数的展开是递归的,从最后开始实例化
1.3 模板模板参数的展开
对于参数列表是模板模板参数,并且是一个可变参数列表的类型,我们同样可以展开,写法上类似,只是繁琐了些。
参考下方代码:
//模板模板参数的展开
template<typename T, template<typename>typename... Container> //泛化版本
class myclasst3 {
public:
myclasst3() {
std::cout << "myclasst3::myclasst3()泛化版本执行了,this = " << this << "\n";
}
};
//偏特化版本
template<typename T, template<typename>typename FirstContainer,
template<typename>typename... OtherContainers>
class myclasst3<T, FirstContainer, OtherContainers...>
:public myclasst3<T, OtherContainers...> {
public:
myclasst3() {
std::cout << "myclasst3::myclasst3()偏特化版本执行了,this = " << this
<< " sizeof...(Others) = " << (sizeof...(OtherContainers)) << "\n";
}
};
同样是继承链展开,我们下面写一个辅助函数来查看调用的信息:
//查看类型的辅助类
template<typename T, template<typename>typename... Container>
class myclasst4 :public myclasst3<T, Container...> {
public:
myclasst4() {
std::cout << "myclasst4::myclasst4()执行了,this = " << this << ", T的类型是:"
<< typeid(T).name() << ",Container的参数个数是" << (sizeof...(Container)) << "\n";
}
};
void Test3() {
myclasst4<int, std::vector, std::list, std::deque, std::stack>myc;
}
这个辅助类继承自泛化版本,因此调用这个辅助类模板的时候,就会展开模板模板类型参数包:
这里使用了
t
y
p
e
i
d
typeid
typeid来打印类型名,可以发现实例化出的容器类都是
i
n
t
int
int类型。
二、通过递归组合方式来展开参数包
2.1 递归组合展开
前面的展开方式基于继承,而递归组合方式是基于构造来展开的。
先看看下面的代码:
//通过递归组合展开
//泛化版本
template<typename... args>
class myclassT {
public:
myclassT() {
std::cout << "myclassT::myclassT()泛化版本执行了,this = " << this << "\n";
}
};
//偏特化版本
template<typename First, typename...Others>
class myclassT<First, Others...> {
public:
First m_i;
myclassT<Others...>m_o;
myclassT() :m_i(0) {
std::cout << "myclassT::myclassT()偏特化版本执行了,this = " << this << "<sizeof...(Others) = "
<< sizeof...(Others) << "\n";
}
//递归构造组合
myclassT(First parf, Others... paro) :m_i(parf), m_o(paro...) {
std::cout << "------------begin------------\n";
std::cout << "myclassT::myclassT(parf,...paro)执行了,this = " << this << "\n";
std::cout << "m_i = " << m_i << "\n";
std::cout << "------------end------------\n\n";
}
};
我们省去了繁琐的继承链,通过在泛化版本内部设置成员变量以及构造函数的方式来展开参数包。
我们会进行递归的构造,直到参数列表为空,调用泛化版本结束递归。
同样的,也能用特殊的特化版本来结束递归:
//特殊的特化版本
template<>
class myclassT<> {
public:
myclassT() {
std::cout << "myclassT::myclassT()特殊的特化版本执行了,this = " << this << "\n";
}
};
运行测试代码:
void Test4() {
myclassT<int, float, double, char>myc(1, 2.0f, 3.0, '4');
}
得到以下结果:
同样是递归展开,所以参数的构造也是从低向上,从右到左。而这一次,由于不是继承关系,所以
t
h
i
s
this
this指针也不同。
非类型参数包和模板模板类型参数包的展开类似,这里就不赘述了。
三、通过元组和递归调用展开参数包
3.1元组的基本使用
元组 t u p l e tuple tuple是 C + + 11 C++11 C++11引入的类型,在元组内可以定义不同的类型,下面是元组的基本使用方法:
//元组的基本使用方法
void Test5() {
//定义不同类型的元素
std::tuple<int, char, float>mytuple(0, '1', 2.0f);
//取出元组第i个元素,使用std::get<>函数模板
std::cout << std::get<0>(mytuple) << "\n";
//后一个类型可以自动推导
std::cout << std::get<1>(mytuple) << "\n";
std::cout << std::get<2>(mytuple) << "\n";
//使用std::get<T>返回元组类型为T的值,如果存在多个相同类型会报错
std::cout << "int类型为:" << std::get<char>(mytuple) << "\n";
}
其中 s t d : : g e t < > std::get<> std::get<>是一个函数模板,可以去除元组第 k k k个类型的数,也可以指定类型取出相应类型的值。
注意的是,如果是取出指定类型的值,必须是唯一的,不然将无法编译通过。
简单测试,运行后得到:
3.2使用元组来递归展开
借助元组可以定义不同类型的这个特点,我们可以进行参数包的展开:
//使用元组来递归展开
template<int mycount, int mymaxcount, typename...T>
class myclassT2 {
public:
static void mysfunc(const std::tuple<T...>& t) {
std::cout << "获取第" << mycount + 1 << "个元素,val = " << std::get<mycount>(t) << "\n";
myclassT2<mycount + 1, mymaxcount, T...>::mysfunc(t); //自递归,每次取出后一个数
}
};
//偏特化版本,用于结束递归
template<int mymaxcount, typename...T>
class myclassT2<mymaxcount, mymaxcount, T...> {
public:
static void mysfunc(const std::tuple<T...>& t) {
std::cout << "调用特化版本结束递归!\n";
}
};
//辅助调用模板
template<typename...T>
void myfunctuple(const std::tuple<T...>& t) {
myclassT2<0, sizeof...(T), T...>::mysfunc(t); //从0开始展开
}
这里, m a x c o u n t maxcount maxcount是需要展开的参数包的总大小,通过 s i z e o f . . . sizeof... sizeof...求得,mycount$是目前展开了多少个参数,初始为0 0 0 0,需要递归 + 1 +1 +1。
因此,当 m a x c o u n t = m y c o u n t maxcount = mycount maxcount=mycount时,需要调用一个特化版本来结束递归。
我们写了一个辅助模板,用于展开:
成员函数使用静态的,这样在编译期间就能确定,不使用静态的也行,只是这样需要创建类对象。
调用测试函数:
void Test6() {
std::tuple<float, int, int>mytuple(1.0f, 2, 3);
myfunctuple(mytuple);
}
成功展开了参数包: