6.2.2 变长模板:模板参数包和函数参数包

我们先看看变长模板的语法,还是以前面提到的tuple为例,我们需要以下代码来声明tuple是一个变长类模板:


template<typename…Elements>class tuple;


可以看到,我们在标示符Elements之前的使用了省略号(三个“点”)来表示该参数是变长的。在C++11中,Elements被称作是一个“模板参数包”(template parameter pack)。这是一种新的模板参数类型。有了这样的参数包,类模板tuple就可以接受任意多个参数作为模板参数。对于以下实例化的tuple模板类:


tuple<int,char,double>


编译器则可以将多个模板参数打包成为“单个的”模板参数包Elements,即Element在进行模板推导的时候,就是一个包含int、char和double三种类型类型集合。

与普通的模板参数类似,模板参数包也可以是非类型的,比如:


template<int…A>class NonTypeVariadicTemplate{};

NonTypeVariadicTemplate<1,0,2>ntvt;


就定义了接受非类型参数的变长模板NonTypeVariadicTemplate。这里,我们实例化一个三参数(1,0,2)的模板实例ntvt。该声明方式相当于:


template<int,int,int>class NonTypeVariadicTemplate{};

NonTypeVariadicTemplate<1,0,2>ntvt;


这样的类模板定义和实例化。

除了类型的模板参数包和非类型的模板参数包,模板参数包实际上还是模板类型的,不过这样的声明会比较复杂,我们在后面再讨论。

一个模板参数包在模板推导时会被认为是模板的单个参数(虽然实际上它将会打包任意数量的实参)。为了使用模板参数包,我们总是需要将其解包(unpack)。在C++11中,这通常是通过一个名为包扩展(pack expansion)的表达式来完成。比如:


template<typename…A>class Template:private B<A…>{};


这里的表达式A…(即参数包A加上三个“点”)就是一个包扩展。直观地看,参数包会在包扩展的位置展开为多个参数。比如:


template<typename T1,typename T2>class B{};

template<typename…A>class Template:private B<A…>{};

Template<X,Y>xy;


这里我们为类模板声明了一个参数包A,而使用参数包A…则是在Template的私有基类B<A…>中,那么最后一个表达式就声明了一个基类为B<X,Y>的模板类Template<X,Y>的对象xy。其中X、Y两个模板参数先是被打包为参数包A,而后又在包扩展表达式A…中被还原。读者可以体会一下这样的使用方式。

不过上面对象xy的例子是基于类模板B总是接受两个参数的前提下的。倘若我们在这里声明了一个Template<X,Y,Z>,就必然会发生模板推导的错误。这跟我们之前提到的“变长”似乎没有任何关系。那么如何才能利用模板参数包及包扩展,使得模板能够接受任意多的模板参数,且均能实例化出有效的对象呢?

事实上,在C++11中,实现tuple模板的方式给出了一种使用模板参数包的答案。这个思路是使用数学的归纳法,转换为计算机能够实现的手段则是递归。通过定义递归的模板偏特化定义,我们可以使得模板参数包在实例化时能够层层展开,直到参数包中的参数逐渐耗尽或到达某个数量的边界为止。下面的例子是一个用变长模板实现tuple(简化的tuple实现)的代码,如代码清单6-10所示。

代码清单6-10


template<typename…Elements>class tuple;//变长模板的声明

template<typename Head,typename…Tail>//递归的偏特化定义

class tuple<Head,Tail…>:private tuple<Tail…>{

Head head;

};

template<>class tuple<>{};//边界条件

//编译选项:g++ -std=c++11 6-2-2.cpp


在代码清单6-10中,我们声明了变长模板类tuple,其只包含一个模板参数,即Elements模板参数包。此外,我们又偏特化地定义了一个双参数的tuple的版本。该偏特化版本的tuple包含了两个参数,一个是类型模板参数Head,另一个则是模板参数包Tail。在代码清单6-10的实现中,我们将Head型的数据作为tuple<Head,Tail…>的第一个成员,而将使用了包扩展表达式的模板类tuple<Tail…>作为tuple<Head,Tail…>的私有基类。这样一来,当程序员实例化一个形如tuple<double,int,char,float>的类型时,则会引起基类的递归构造,这样的递归在tuple的参数包为0个的时候会结束。这是由于我们定义了边界条件或者说初始条件,即tuple<>这样不包含参数的偏特化版本而造成的。在代码清单6-10中,tuple<>偏特化版本是一个没有成员的空类型。这样一来,编译器将从tuple<>建造出tuple<float>,继而造出tuple<char,float>、tuple<int,char,float>,最后就建造出了tuple<double,int,char,float>类型。

图6-2是tuple<double,int,char,float>实例化后的继承结构示意图。我们用方框表示类型,而方框内的方框则表示类型由其内部的方框所代表的类型私有派生而来。

6.2.2 变长模板:模板参数包和函数参数包 - 图1

图 6-2 tuple<double,int,char,float>继承结构示意图

这种变长模板的定义方式稍显复杂,不过却有效地解决了模板参数个数这样的问题。当然,这样做的前提是模板类/函数的定义要具有能够递推的结构。

我们再来看一个使用非类型模板的一个例子,如代码清单6-11所示。

代码清单6-11


include <iostream>

using namespace std;

template<long…nums>struct Multiply;

template<long first,long…last>

struct Multiply<first,last…>{

static const long val=first*Multiply<last…>::val;

};

template<>

struct Multiply<>{

static const long val=1;

};

int main(){

cout<<Multiply<2,3,4,5>::val<<endl;//120

cout<<Multiply<22,44,66,88,9>::val<<endl;//50599296

return 0;

}

//编译选项:clang++-std=c++11 6-2-3.cpp


在代码清单6-11中,我们定义了接受非类型参数的变长模板类Multiply。Multiply的唯一用途是将模板的参数相乘。而通过Mutiply的成员val,我们可以将参数相乘的结果读出来。类似地,这里也发生了编译时期的值计算,我们最后在main函数中打印的是Multiply的一个常量静态成员val,而非执行任何函数调用。本例跟代码清单6-8的例子非常相似,也可以算作模板元编程的范畴。

除了变长的模板类,在C++11中,我们还可以声明变长模板的函数。对于变长模板函数而言,除了声明可以容纳变长个模板参数的模板参数包之外,相应地,变长的函数参数也可以声明成函数参数包(function parameter pack)。比如:


template<typename…T>void f(T…args);


这个例子中,由于T是个变长模板参数(类型),因此args则是对应于这些变长类型的数据,即函数参数包。值得注意的是,在C++11中,标准要求函数参数包必须唯一,且是函数的最后一个参数(模板参数包没有这样的要求)。

有了模板参数包和函数参数包两个概念,我们就可以实现C中变长函数的功能了。我们可以看看这个C++11提案中实现新的printf的例子,如代码清单6-12所示。

代码清单6-12


include <iostream>

include <stdexcept>

using namespace std;

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");

}

int main(){

Printf("hello%s\n",string("world"));//hello world

}

//编译选项:g++ -std=c++11 6-2-4.cpp


在代码清单6-12中,实现了Printf的参数。Printf是一个变长函数模板,这里为了兼容于printf,仍然提供了printf式的字符串,所以Printf功能等同于printf,可以接受该字符串及变长的参数。而Printf的功能也大于printf,因为它可以接受std::string这样的非内置类型。

从代码清单6-12的代码中我们看到,相比于变长函数,变长函数模板不会丢弃参数的类型信息。因此重载的cout的操作符<<总是可以将具有类型的变量正确地打印出来。这就是Printf功能大于printf的主要原因,也是变长模板函数远强于变长函数的地方。