vlambda博客
学习文章列表

【C++11】消除重复, 提升代码质量---可变参数模板

在C++11之前,类模板或者模板函数的模板参数是固定的,从C++11开始,C++标准委员会增强了模板的功能,新的模板特性允许在模板定义中模板参数可以包含零到无限个参数列表,声明可变参数模板时主要是在class和typename后面添加省略号的方式。省略号的作用如下:

  • 声明一个参数包,这个参数包中可以包含0到任意个模板参数;

  • 在模板定义的右边,可以将参数包展开成一个个独立的参数;

1 可变参数模板函数

可变参数模板函数代码如下所示:

template <class ... T>void Fun(T ... args){ cout<<sizeof...(args)<<endl;}int main(){ Fun(); Fun(3,"23"); Fun(3,"23",4); return 0;}

上面的代码运行结果为:

023

代码中,分别调用了Fun的三个重载函数,第一个参数包中参数个数为0,第二个为2,第三个为3,所以在输出的时候结果分别为0,2,3。参数包的展开方式有两种,分别是递归函数方式展开、逗号表达式和函数初始化列表方式展开。

1.1 递归方式展开

同递归算法一样,使用递归方式展开需要提供一个参数包展开函数和一个递归结束函数,具体代码如下:

template <class T>void print(T t){ cout<<t<<endl;}
template <class T,class ... Args>void print(T head,Args ... rest){ cout<<"parameters:"<<head<<endl; print(rest ...);}int main(){ print(2); print(3,"23"); print(3,"23",4); return 0;}

代码执行结果如下:

2parameters:323parameters:3parameters:234

上面的两个函数一个是递归函数,一个是递归终止函数,没输出一个参数,调用一次参数包的参数个数就会减少一个,直到所有的参数输出完为止。

1.2 逗号表达式和初始化列表方式

逗号表达式的优点是不需要提供一个终止函数,就像在本文一开始提供的代码那样。下面我们用改方式实现递归打印参数的功能。

template <class T>void print(T t){ cout<<t<<endl;}
template <class ... Args>void printAll(Args ... rest){ int arr[]={(print(rest),0)...};}int main(){ printAll(1,2,3,4); return 0;}

代码运行结束后,同样会输出结果为:1,2,3,4。

printAll函数中,每输出一个参数都会调用一次print函数,每次执行成功后,都可以得到逗号表达式的值0,这样实际上就是对一个可变数组arr[]进行了一次初始化,在实行过程中,(print(rest),0)...会被替换成(print(1),0)(print(2),0),(print(3),0),(print(4),0)。按照优先级顺序,逗号表达式优先级最低,在执行逗号表达式前都会调用print输出参数。

在上面printAll函数还可以继续进行优化,如使用std::initializer代替数组,使用lanbda代替print函数。优化后的结果如下

template <class ... Args>void printAll(Args ... rest){ std::initializer_list<int>{([&]{cout<<rest<<endl;}(),0)...};}

执行程序会会得到同样的结果。

2 可变参数模板类

可变参数模板类实际上就是一个模板类,参数是可变的,在C++11中,元组类std::tuple就是一个可变参数的模板类。可变参数模板类参数包展开时主要通过模板特化和继承的方式进行。

std::tuple的原定如下:

template <class... Types> class tuple;


2.1 模板递归和特殊方式展开参数包

可变参数模板类在定义时一般需要2-3个类。主要包括类的声明、类的特化,如下面的参数模板类就定义了三个类。代码如下:

//前向声明template <class ... Args> struct sum;//基本定义template<typename First,typename ... Rest>struct sum<First,Rest ...> : std::integral_constant<int,sum<First>::value+sum<Rest ...>::value>{};
//递归终止template <typename Last>struct sum<Last> : std::integral_constant<int,sizeof(Last)>{};

在上面的代码中,主要包含了3个部分,第一部分是前向声明,声明了一个可变参数的模板类。第二部分是类的定义,在第二部分中实现了部分可展开的参数模板类。第三部分就是就是特化的递归终止类。

 也可以改变代码,省去前向声明,限制至少有一个参数,代码如下:

//基本定义template<typename First,typename ... Rest>struct sum{ enum {value = sum<First>::value + sum<Rest ...>::value };};
//递归终止template <typename Last>struct sum<Last>{ enum{value = sizeof(Last) };};

代码执行后,结果是一样的。

2.2 继承方式展开参数包

可变参数类比可变参数函数模板要复杂,但是功能也会更加强大,因为可变参数模板类可以具备状态,和type_traits联合使用后可以在编译器对类型进行判断、选择和转换等操作。

下面的代码展示了通过继承方式展开参数包的方法。

template <int ...>struct IndexSeq{};
template<int N,int ... Indexes>struct MakeIndexes : MakeIndexes<N-1,N-1,Indexes ...>{};//模板特化展开模板参数包template<int ... Indexes>struct MakeIndexes<0,Indexes ...>{ typedef IndexSeq<Indexes ...> type;};

在上面的代码中MakeIndexes 继承自身的转化模板类,这个特化的模板类同时也在展开参数包,这个展开过程通过继承发起,直到遇到终止的特化 参数模板类。

3 可变参数模板消除重复代码

可变参数模板的特性之一就是参数包中的参数数量和类型可以是任意的,因此可以通过泛化的方式处理问题。如下面打印函数代码:

template <typename T>void Print(T t){ cout<< t <<", "<<endl;}
template<typename T,typename ... Args>void Print(T t,Args ... Rest){ cout<<t<<" "; Print(Rest...);}

上面的代码可以打印任意个数和类型参数试想,如果使用传统的方法实现。需要写多个重载函数,如果后面进行扩展,也需要不断的新增重载函数。

除此之外,在C++11之前,定义一个工厂类,需要写很多的重载函数,进而创建不同的实例,使用范化后,只需要一个可变参数模板就可以支撑很多功能。示例代码如下:

template <typename ... T>T* Instance(T&& ...t){ return new T(std::forward<T>(t)...);}

- EOF -


扫码关注


图文:龙小

排版:龙小