一个简单的metaprogramming实例:
一个计算从1加到100的例子:
1,用普通函数递归实现
//用普通函数实现递归
double SumFunc(int N){
double sum = N;
//递归结束开始返回的条件
if (N == 1){
return sum;
}
else{
//递归展开代码
sum = sum + SumFunc(--N);
//解除递归要返回sum
return sum;
}
}
画图分析:
所以用普通函数实现递归有两个重要的关节:
1)结束递归
if (N == 1){
return sum;
}
2)每个函数递归被解除了要返回当前的sum
//递归展开代码
sum = sum + SumFunc(--N);
//解除递归要返回sum
return sum;
看了普通函数实现1加到100的递归之后,我们觉得似乎有点复杂,并且开销也很大,比如100次的函数调用,其中包含了100次的变量sum的定义,因此这100次的函数的进栈出栈导致了其效率的低下,接下来让我们以一种焕然一新的方法来对待从1加到100:
2,用模板递归实现:
//用模板递归来实现
template<int N>
class SUM{
public:
//递归实例化
enum{ sum = N + SUM<N - 1>::sum };
};
//用来结束递归的全局特化
template<>
class SUM<0>{
public:
enum{ sum = 0 };
};
画图分析:
所以模板的递归很简单,就是一直往前加,一直碰到递归条件!所以他的开销大致可以看为:100+99+89+88+...+1递加的开销+100次实例化的开销!
这里或许不应该拿函数递归和模板递归比较效率,但是当我们遇到用递归实现某个东西的时候,我们还是选择模板递归吧,毕竟函数递归开销真的很凶!另外,模板递归是在编译期完成的——这也是我们本篇讨论的核心主题!
上例中能否用static const成员变量替换enum:
//用模板递归来实现
template<int N>
class SUM{
public:
//递归实例化
static int const sum = N + SUM<N - 1>::sum;
//enum{ sum = N + SUM<N - 1>::sum };
};
//用来结束递归的全局特化
template<>
class SUM<0>{
public:
static int const sum = 1;
//enum{sum=1};
};
因为我们要的是编译期就完成计算,而static const的值只有在分配了内存之后才能得到(而内存的分配是在运行期进行的),因此这里不能使用static const代替enum
一个在模板递归中减少实例化次数的方法
一个计算平方根的例子:
#include<iostream>
using namespace std;
template<int N,int LO=0,int HI=N>
class Sqrt{
public:
enum{ mid = (LO+HI+1)/2 };
enum{ result = (N < mid*mid) ? Sqrt<N, LO, mid - 1>::result
: Sqrt<N,mid,HI>::result};
};
template<int N,int M>
class Sqrt<N, M, M>{
public:
enum{result=M};
};
void main(){
cout << Sqrt<16>::result<< endl;
}
分析:
1)"?:"是个大问题:
"?:"操作符会导致一个问题,如果条件满足,那么黄线部分会进行完全实例化,但不幸的是红线部分也会进行完全的实例化,因此无论条件满足与否,黄线和红线部分都会进行完全实例化,对于大多数编译器而言,实例化的开销都是很大的,而基于上面这个示例,实例化的次数将达到两倍,因此将是一个很大的开销!
2)上面的设计属于一个很危险的设计:
我们使用上面的设计,假设红线部分虽然开销大,但是最终会归于特化“class Sqrt<N, M, M>”,因此其特化将被终止,但是如果红线部分触碰不到终止条件,那么其特化将是无穷尽的,这也将触发一个编译错误:(递归类型或函数依赖项上下文太复杂 )(因为编译器对一个嵌套实例化的个数是有限制的),因此我们不但要承受不必要的实例化开销,还要精心预防无穷实例化的危险,由此可见上面的“?:”是个糟糕的设计!
既然罪魁祸首(Sqrt<N,mid,HI>::result)不但导致了不必要的实例化开销,也有发生无穷实例化的危险,那么我们就需要一个新的设计来避免这两个问题:因此我们要设计一种策略让条件满足的时候不要着急去获取实例化体的成员,让实例化体的成员等到类型确定之后再获取!毕竟我们上面的设计是类型的确定和result的获取同步进行的(Sqrt<N,mid,HI>::result)
解决办法:
IfThenElse头文件:
#ifndef IFTHENELSE
#define IFTHENELSE
//IfThenElse的作用:用来根据给定的条件抉择类型
template<bool C, typename Ta, typename Tb>
class IfThenElse;
//对于true的特化
template< typename Ta, typename Tb>
class IfThenElse<true, Ta, Tb>{
public:
typedef Ta ResultType;
};
//对于false的特化
template< typename Ta, typename Tb>
class IfThenElse<false, Ta, Tb>{
public:
typedef Tb ResultType;
};
#endif
测试源文件:
#include<iostream>
#include"IfThenElse.h"
using namespace std;
template<int N,int LO=0,int HI=N>
class Sqrt{
public:
enum{ mid = (LO+HI+1)/2 };
typedef typename IfThenElse < (N < mid*mid),
Sqrt<N, LO, mid - 1 >,
Sqrt < N, mid, HI >> ::ResultType SubT;
enum{result=SubT::result};
};
template<int N,int M>
class Sqrt<N, M, M>{
public:
enum{result=M};
};
void main(){
cout << Sqrt<16>::result<< endl;
}
1)第一个问题是怎么解决的?
关键在于这里:typedef Sqrt<N,LO,mid-1> Subt——"为一个类模板定义一个typedef并不会导致编译器实例化该实例的实体(最起码不会触发全部实例化)",这时实例化的次数将接近log2(N);
2)第二个问题是怎么解决的?
既然第二个问题的本质是由于完全实例化而导致了递归的产生,这里并没有发生完全实例化,将不存在无穷的递归实例化问题!
(其中和IfThenElse相关的事情在这篇文章中《trait和policy 》)
应用IfThenElse设计的另一个示例:
我们将重新设计上个示例计算平方根的程序,并且使其更加自然(接近metaprogramming的本质)、更加迭代,毕竟上个示例为了计算平方根,将程序设计得稍显复杂!
#include<iostream>
using namespace std;
template<int N,int I=0>
class Sqrt{
public:
enum{ result = (I*I<N)?Sqrt<N,I+1>::result:I };
};
template<int N>
class Sqrt<N, N>{
public:
enum{result=N};
};
void main(){
cout << Sqrt<4>::result << endl;
}
我们又将老调重弹,为什么要做这样的设计(似乎根本用不到):
template<int N>
class Sqrt<N, N>{
public:
enum{result=N};
};
答案是防止无穷递归!
让我们具体来详看一下无穷递归的”模样“:
当然如果你就愿意使用"?:"操作符并且能提供准确的终止无穷递归的条件,但是我们刚才的分析显示还有一个问题或许你是无法接受的,假如我们计算的是16的平方根呢?那么我们真正想要的实例化到Sqrt<16,4>的时候就完成了(大概接近4次),其后的接近12次将是你要无辜付出的,而且这还不算是很严重的问题(如果你不在乎额外开销的话),重要的问题是假如我们的编译器允许我们的最大嵌套实例化次数是100次,那么我们就只能计算最大值到100的平方根(前面10次我们需要的,后面90次必须付出的!),这或许是一个我们无法接受的事实!
让我们使用IfThenElse来终结吧
#include<iostream>
#include"IfThenElse.h"
using namespace std;
template<int N>
class Value{
public:
enum{result=N};
};
template<int N,int I=0>
class Sqrt{
public:
typedef typename IfThenElse<(I*I<N),
Sqrt<N, I + 1>,
Value < I >>::ResultType SubT;
enum{ result = SubT ::result};
};
void main(){
cout << Sqrt<4>::result << endl;
}
可见,用IfThenElse改进之后的设计中并没有终止条件的代码,而是新添了一个Value的类模板用来配合IfThenElse,这样,假如我们的编译器允许我们的最大嵌套实例化次数是100次,那么我们就能计算最大值到4096的平方根!
到这里,我们来归纳一下template metaprogramming具备的设计特点:
一个template metaprogramming设计可以包含哪些:
1)状态变量:也就是模板参数
2)迭代构造:通过递归
3)路径选择:通过使用条件表达式或者特化
4)整形(即枚举里面的值应该是整形)算法
template metaprogramming的局限:
1)模板实例化需要耗费巨大的编译器资源
2)扩展的实例化会很快地降低编译器的效率,甚至耗光所有的编译器资源
3)C++标准建议最多只进行17层的递归实例化(这其实不属于缺陷,只是这一条没处安置了)
递归实例化和递归模板实参
一个例子:
#include<iostream>
using namespace std;
template<typename T,typename U>
class X{};
template<int N>
class Trouble{
public:
typedef X<typename Trouble<N - 1>::Type, typename Trouble<N - 1>::Type> Type;
};
template<>
class Trouble<0>{
public:
typedef int Type;
};
void main(){
Trouble<4>::Type el;
}
上面的代码复杂度体现在:
typedef X<typename Trouble<N - 1>::Type, typename Trouble<N - 1>::Type> Type;
这句代码可谓是很糟:
1)编译器将产生接近N个实例化体(这在前面已经论述过了)
2)最主要的原因:Trouble<N>::Type的复杂度与2的N次方成正比:Trouble<0>::Type为int,那么Trouble<1>::Type为X<double,double>,Trouble<2>::Type为X<X<double,double>,X<double,double>>......
(编译器要为每个类型生成一个mangled name,而mangled name需要根据模板特化进行组织,早起C++实现mangled name的长度粗略等于template-id的长度,因此Trouble<10>::Type将会产生一个长度大于等于10000个字符的mangled name,幸运的是现在的C++实现中采用嵌套型的template-id,使用了智能压缩技术,因此Trouble<10>::Type或许只有几百个字符,但是我们仍然需要避免在模板实参中使用递归嵌套实例化!)
使用metaprogram来展开循环——线性加法
一个普通的例子实现两容器元素乘积之和(元素类型、个数相同):
#include<iostream>
using namespace std;
template<typename T>
inline T AddFunc(int dim, T * a, T * b){
T result = T();
for (int i = 0; i < dim;i++){
result += a[i] * b[i];
}
return result;
}
void main(){
int a[3] = { 1, 2, 3 };
int b[3] = { 1, 2, 3 };
cout << AddFunc(3,a, b) << endl;
/*
输出结果:
14
*/
}
使用metaprogram实现:
#include<iostream>
using namespace std;
template<int Dim,typename T>
class Add{
public:
static T result(T * a, T * b){
return *a * *b + Add<Dim - 1, T>::result(a + 1, b + 1);//a+1向后推移
}
};
//当Dim到1的时候,就是成员指针到末尾的时候
template<typename T>
class Add<1, T>{
public:
static T result(T * a,T * b){
return *a * *b;
}
};
//辅助函数
template<int Dim,typename T>
inline T AddFunc(T * a,T * b){
return Add<Dim, T>::result(a, b);
}
void main(){
int a[3] = { 1, 2, 3 };
int b[3] = { 1, 2, 3 };
cout << AddFunc<3>(a, b) << endl;
/*
输出结果:
14
*/
}
使用metaprogram实现最大的好处是只用在一处计算,别处再使用的时候将不再执行计算步骤:
//第一次实例化Add<3,int>::result(a,b);
cout << AddFunc<3>(a, b) << endl;
//第二次直接使用实例化体
cout << AddFunc<3>(a, b) << endl;
//此时Add<3,int>中的result相当于这样
static int result(int *,int *){
return 14;
}
这不失为一种避免重复计算的方法,但是也是有很多需要克服的方面,例如发生了Dim次的实例化,Dim必须小于编译器所要求的的最大的嵌套实例化的层数,增加了编译期时间开销、大量的POI的管理等,因此metaprogram有其用武之地,也有其不能用之地!