6.2.3 变长模板:进阶
在代码清单6-10中,我们看见参数包在类的基类描述列表中进行了展开,而在代码清单6-11中,参数包则在表达式中进行了展开。那么在C++11中,有多少地方可以展开参数包呢?事实上,标准定义了以下7种参数包可以展开的位置:
❑表达式
❑初始化列表(列表初始化参见第3章)
❑基类描述列表
❑类成员初始化列表
❑模板参数列表
❑通用属性列表(第8章中会讲到)
❑lambda函数的捕捉列表(第7章中会讲到)
语言的其他“地方”则无法展开参数包。而对于包扩展而言,其解包也与其声明的形式息息相关。事实上,我们还可以声明一些有趣的包扩展表达式。比如声明了Arg为参数包,那么我们可以使用Arg&&…这样的包扩展表达式,其解包后等价于Arg1&&,…,Argn&&(这里我们假设Arg1是参数包里的第一个参数,而Argn是最后一个)。
一个更为有趣的包扩展表达式如下:
template<typename…A>class T:private B<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>{};
即派生于多参数的模板类的派生类,这点存在着本质的不同。
类似的状况也会发生在函数声明上,我们可以看看代码清单6-13所示的例子。
代码清单6-13
include <iostream>
using namespace std;
template<typename…T>void DummyWrapper(T…t){}
template<typename T>T pr(T t){
cout<<t;
return t;
}
template<typename…A>
void VTPrint(A…a){
DummyWrapper(pr(a)…);//包扩展解包为pr(1),pr(",")…,pr(",abc\n")
};
int main(){
VTPrint(1,",",1.2,",abc\n");
}
//编译选项:xlC-+-qlanglvl=variadictemplates 6-2-5.cpp
在代码清单6-13中,我们定义了一个依赖于变长模板包扩展展开的VTPrint函数。可以看到,pr(a)…这样的包扩展表达式,将会被展开为pr(1)、pr(",")、…、pr(",abc\n"),从而pr将参数以此打印出来。在我们的实验机上,我们可以得到以下输出:
1,1.2,abc
这样一来,我们也实现一个接受任意长度参数的打印函数。可以看到,这样的包扩展表达式,为参数包的使用带来了非常多的灵活性。
注意 在我们实验机上使用g++编译上述例子将无法得到上面的答案(g++似乎是被每个pr的执行顺序打乱了)。
以上讲的都是参数包的扩展,事实上,除去扩展外,在C++11中,标准还引入了新操作符“sizeof…”(没有看错,这个操作符就是sizeof后面加上了3个小点),其作用是计算参数包中的参数个数。通过这个操作符,我们能够实现参数包更多的用法。我们来看一个例子,如代码清单6-14所示。
代码清单6-14
include <cassert>
include <iostream>
using namespace std;
template<class…A>void Print(A…arg){
assert(false);//非6参数偏特化版本都会默认assert(false)
}
//特化6参数的版本
void Print(int a1,int a2,int a3,int a4,int a5,int a6){
cout<<a1<<","<<a2<<","<<a3<<","
<<a4<<","<<a5<<","<<a6<<endl;
}
template<class…A>int Vaargs(A…args){
int size=sizeof…(A);//计算变长包的长度
switch(size){
case 0:Print(99,99,99,99,99,99);
break;
case 1:Print(99,99,args…,99,99,99);
break;
case 2:Print(99,99,args…,99,99);
break;
case 3:Print(args…,99,99,99);
break;
case 4:Print(99,args…,99);
break;
case 5:Print(99,args…);
break;
case 6:Print(args…);
break;
default:
Print(0,0,0,0,0,0);
}
return size;
}
int main(void){
Vaargs();//99,99,99,99,99,99
Vaargs(1);//99,99,1,99,99,99
Vaargs(1,2);//99,99,1,2,99,99
Vaargs(1,2,3);//1,2,3,99,99,99
Vaargs(1,2,3,4);//99,1,2,3,4,99
Vaargs(1,2,3,4,5);//99,1,2,3,4,5
Vaargs(1,2,3,4,5,6);//1,2,3,4,5,6
Vaargs(1,2,3,4,5,6,7);//0,0,0,0,0,0
return 0;
}
//编译选项:g++ -std=c++11 6-2-6.cpp
在代码清单6-14的例子当中,我们仅为变长函数模板Print提供了一个参数为6的特化版本。假如我们的代码试图实例化参数不等于6的Print,编译器则会推导Print为执行assert(false)的操作的版本。反之,则会实例化参数为6的特化版本。这里,Vaargs的函数参数包的参数数量被计算并被传递,进而实现不同参数数量的不同打印。这里同样实现了接受任意个参数的功能,不过却没有像之前一样使用递归。
有的读者一定会对使用模板做变长模板的参数包感兴趣。实际上,使用模板做参数包跟使用类型和非类型的模板参数包并没有太大的区别。我们可以看看代码清单6-15所示的这个例子。
代码清单6-15
template<typename I,template<typename> class…B>struct Container{};
template<typename I,template<typename> class A,template<typename> class…B>
struct Container<I,A,B…>{
A<I>a;
Container<I,B…>b;
};
template<typename I>struct Container<I>{};
//编译选项:g++ -std=c++11-c 6-2-7.cpp
代码清单6-15就定义了使用模板作为变长模板参数包的一个变长模板。可以看到,Container类型使用了两个参数——类型I和模板参数包template<typename> class…B。而递归的偏特化定义中,我们看到成员变量的定义使用了类型I和模板A:A<I>这样的模板中的类型声明。即用I做参数实例化模板参数template<typename> class A(希望读者还没有糊涂)。而Container<I,B…>b则保证编译器会继续递归地推导下去。推导到模板参数包为空的时候,我们的边界条件就会起作用。代码清单6-15整段代码并没有特别之处,如果读者在这里觉得阅读代码有点儿困难,那可能需要花一点儿时间编写代码来进行理解。
那么,在C++11中,我们是否可以同时在变长模板类中声明两个模板参数包呢?事实上,如果我们像下面这样声明,通常就会发生编译错误:
template<class…A,class…B>struct Container{};
template<class…A,class B>struct Container{};
编译器会提示程序员,模板参数包不是变长模板类的最后一个参数。不过实际上,如果编译器能够进行推导的话,模板参数包倒不一定非得作为模板的最后一个参数,比如代码清单6-16所示的这个例子[1]。
代码清单6-16
include <cstdio>
include <tuple>
using namespace std;
template<typename A,typename B>struct S{};
//两个模板参数包
template<
template<typename…>class T,typename…TArgs
,template<typename…>class U,typename…UArgs
>
struct S<T<TArgs…>,U<UArgs…>>{};
int main()
{
S<int,float>p;
S<std::tuple<int,char>,std::tuple<float> >s;
}
//编译选项:g++ -std=c++11 6-2-8.cpp
代码清单6-16中,我们使用了两个模板参数包作为模板类S的参数。很有意思的是,除了struct S以外,另外两个模板参数:class T和class U也是变长模板,因此本例实际是一个变长模板作参数的模板示例。值得注意的是,模板中的变长模板是无法做递归的偏特化声明和定义边界条件的特化声明的。不过在本例中,我们看到了模板类struct S依然是可以推导的。这是因为我们使用了标准库中的变长模板类型tuple。那么T和U的推导就可以根据tuple的定义进行,直至推导到边界条件(T和U两个tuple都不再包含模板参数),即template<typename A,typename B>struct S{};,S的定义才递归地被产生。
代码清单6-16的写法略有些晦涩,不过至少成功地同时地使用两个模板参数包。如果读者需要更多的模板参数包,也可以“依葫芦画瓢”。
我们再来看看变长模板参数和完美转发结合使用的例子,如代码清单6-17所示。
代码清单6-17
include <iostream>
using namespace std;
struct A{
A(){}
A(const A&a){cout<<"Copy Constructed"<<func<<endl;}
A(A&&a){cout<<"Move Constructed"<<func<<endl;}
};
struct B{
B(){}
B(const B&b){cout<<"Copy Constructed"<<func<<endl;}
B(B&&b){cout<<"Move Constructed"<<func<<endl;}
};
//变长模板的定义
template<typename…T>struct MultiTypes;
template<typename T1,typename…T>
struct MultiTypes<T1,T…>:public MultiTypes<T…>{
T1 t1;
MultiTypes<T1,T…>(T1 a,T…b):
t1(a),MultiTypes<T…>(b…){
cout<<"MultiTypes<T1,T…>(T1 a,T…b)"<<endl;
}
};
template<>struct MultiTypes<>{
MultiTypes<>(){cout<<"MultiTypes<>()"<<endl;}
};
//完美转发的变长模板
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<MultiTypes>(a,b);
}
//编译选项:g++ -std=c++11 6-2-9.cpp
在代码清单6-17中,我们定义了两个类型A和B。此外,我们还定义了变长模板MultiType,以及一个接受变长模板作为参数的变长模板函数Build。可以看到,在Build的声明中,我们将其参数声明为一个右值引用,而我们在转发时,则使用了std::forward来保证左值按照左值引用、右值按照右值引用的方式传递。在我们的这段代码示例中,main函数传递了两个左值给Build<MultiTypes>作为变长函数包。这里,我们通过Multitypes的构造函数来递归地构造一个MultiTypes的实例。编译运行该程序,在我们的实验机上,可以看到如下结果:
MultiTypes<>()
MultiTypes<T1,T…>(T1 a,T…b)
MultiTypes<T1,T…>(T1 a,T…b)
虽然代码清单6-17所示的代码看起来非常复杂,不过其产生的效果却非常好。事实上,由于我们传递的是左值,因此在Multitypes对象在构造的时候,没有调用任何的拷贝构造函数或者移动构造函数。构造后的类型实际上只包含了对之前定义变量a和b的引用。我们可以通过图6-3来看一下。
图 6-3 代码清单6-17中构造的变量类型
虽然在语法和编程上,使用变长模板会存在一些难度,不过对于库的编写者而言,变长模板具备了很好的实用性(尤其是它能够实现其他方式无法实现的功能)。在标准库中,也添加了形如tuple、emplace_back这样的变长模板类和变长模板函数。如果读者需要传递变长的类型或者函数参数,也不妨使用变长模板试试。
[1]本例来源于http://stackoverflow.com/questions/4706677/partial-template-specialization-with-multiple-template-parameter-packs。