详述trait和policy演化

来源:互联网 发布:高通cdma专利到期 知乎 编辑:程序博客网 时间:2024/05/18 06:19

http://blog.csdn.net/arau_sh/article/details/5874510

转自http://redwolf.blog.51cto.com/427621/90096

 

       开博时发了一篇“trait和policy研习”的源码,很少有朋友关注,写的太简略,这一次把它展开来,发现一下子还写不完,分两次吧!希望和朋友们一起学习!
        给定一个整型数组,要求编写一个函数,求其内元素的和。这是一个简单的编程,大家都能很快的编写出如下的程序:
#define SIZE 5;  
int a[SIZE]={10,11,12,13,14};  
int Toatal(const int a[], int size)  
{  
 int i=0, total=0;  
 while(i<size)  
 {  
  total+=a[i]  
  i++;  
 }  
 return total;  
}
也可能有人这么写:
int Total(const int* a0, const int* an) 

 int total=0; 
 while(a0!=an) 
 { 
  total+=*a0; 
  a0++; 
 } 
 return total; 
上面的程序有问题吗?我们向下看。
我们把第二种形式的求和函数改写成模板,试图使之适用于更多的类型。
template<typename T> 
T Total(const T* beg, const T* end) 

 T total=0; 
 while(beg!=end) 
 { 
  total+=*beg; 
  beg++; 
 }  
 return total; 
上面的程序有问题吗?细心的朋友可能会想到:T total=0;我们不能用0去初始化一个类型为T的对象,我们不知道T具体是什么样的,最好的方法是显示的调用T的默认构造函数,于是我们可以改一下:T total=T();
由于使用了模板,我们可以对不同的类型值求和,我们测试下面两个数组:
int a[5]={0,1,2,3,4}; 
char b[5]={'a', 'b', 'c', 'd', 'e'}; 
void main() 

 int sum0=Total(a, a+5); 
 int sum1=Total(b, b+5); 
结果你会发现:sum0=10(没有问题),sum1=-17(问题在哪里?)
问题在于,T total=T();数组b是一个char型的,total也是一个char型的,而char型值的范围很小,累加很容易越界。于是就出现了-17.其实这种错误很容易出现在我们的编程中,特别是在嵌入式编程中,很多时候有人建议用char型(其实也未必,内部有对齐问题),一不小心就出错了。上面两个C函数,其实也存在这个问题,多个整型值的和不一定还在整型范围内。这在我们的编程中应该注意。
 
如此看来,我们有必要把模板函数修改一下,最直接和容易想到的是:
template<typename R, typename T> 
R Total(const T* beg, const T* end) 

 R total=R(); 
 while(beg!=end) 
 { 
  total+=*beg; 
  beg++; 
 }  
 return total; 
这时,我们的调用大概如此:int sum1=Total<int>(b,b+5);于是,我们得到了正确的结果495。注意:这里R和T的位置顺序不能颠倒。这个方案看起来不错,“但是我们仍然期望完全避免这个约束”(《C++ Templates中文版》语)。在我看来,我更习惯于这样的调用形式:int sum0=Total(b, b+5);而非int sum1=Total<int>(b,b+5)。
 
C++前辈们给我们展示了另外一种解决方案:
声明一个类:
template<typename T> class TotalTrait; 
或者定义一个类:
template<typename T> class TotalTrait{};(
(这里我测试的情况看,都可以,但相关书籍上用的是声明形式的,大概与编译有关,因为这里我们只关心特化的类)下面我们通过显示特化,使得另外一种类型R与T相关联:
template<> 
class TotalTrait<char

public
 typedef int R; 
}; 
这个特化的意思大体表述如下:如果T是char的话,定义R的类型为int。
相似的我们可以写出很多特化类:
template<> 
class TotalTrait<int

public
 typedef long R; 
}; 
template<> 
class TotalTrait<float

public
 typedef double R; 
}; 
这里,我们就为Total创建了Trait类,并提供了3种特化。
于是我们再改写一下上面的模板:
template<typename T> 
typename TotalTrait<T>::R Total(const T* beg, const T* end) 

 typedef typename TotalTrait<T>::R R; 
 R total=R(); 
 while(beg!=end) 
 { 
  total+=*beg; 
  beg++; 
 } 
 return total; 
这里对typename的另一种用法稍作解释:typename TotalTrait<T>::R Total(const T* beg, const T* end)和typedef typename TotalTrait<T>::R R;中的typename是一种声明的用法,它声明TotalTrait<T>::R是模板类TotalTrait<T>里定义的一种类型。看下面一个例子:TotalTrait<T>::R *m;本意是定义一个TotalTrait<T>::R类型的指针m,但由于没有typename声明类型,编译器会认为R是TotalTrait<T>中的一个静态成员,于是变成了两个变量相乘的语意。

在主程序中调用上面的模板更符合传统风格:int sum1=Total(b, b+5);结果也是正确的495.
如果有一个新的类型,我们只需要提供一个显示的关于Total的特化类,就能够让Total函数正常运行。如:我们自定义了一个类A,同时求和后结果会是B类型,于是:
template<> 
class TotalTrait<A> 

public
 typedef B R; 
}; 
已知:A c[5]={...}
调用:B sum=Total(c, c+5);
关于返回值引发的问题,我们通过一系列的尝试,找到了几个解决方法,都取得了不错的结果。关于这个问题,我们暂时只讨论这么多。

上面的程序中,还有一个细节曾经被我们忽略。我们初始化R total=R()时,能够保证总是返回一个合适的值吗?对于多数的内建类型如char、int、long等,在我们的Total函数中都能够产生正确的初始值。但是如果是一个用户定义的类A或者外部引用的类B呢?或者某个
类C的C()={12,34,56}呢?显然不行。我们再考虑,如果我们吧累和变成累积呢?显然初始化成0不正确。所以这个问题我们必须要认真面对,我们需要一个显式的正确的初值定义。解决这个问题,我们依然可以用TotalTrait类,但用第一个模板方式似乎无能为力。
于是我们扩充TotalTrait定义:
template<> 
class TotalTrait<char

public
 typedef int R; 
 static const R zero=0; 
}; 
template<> 
class TotalTrait<int

public
 typedef long R; 
 static const R zero=0; 
}; 
这里特化时,在类中增加了一个静态常型成员zero,并初始化为0。
再修改一下上面的模板:
template<typename T> 
typename TotalTrait<T>::R Total(const T* beg, const T* end) 

 typedef typename TotalTrait<T>::R R; 
 R total=TotalTrait<T>::zero; 
 while(beg!=end) 
 { 
  total+=*beg; 
  beg++; 
 } 
 return total; 
ok,一切正常。我们特化更多:
class TotalTrait<float

public
 typedef double R; 
 static const R zero=0.0; 
}; 
sorry,编译通不过:
d:/redwolf/redwolf/base/trait.h(156) : error C2864: “redwolf::base::TotalTrait<float>::zero”: 只有静态常量整型数据成员才可以在类中初始化
生成日志保存在“file://d:/redwolf/redwolf/Debug/BuildLog.htm”
标准C++,只允许在类中对整型成员和枚举类型静态常量成员初始化。
我相信,一些朋友看着看着就有点晕头了,不过没关系,你如果一边看一边在编程测试的话,体会会深一点,然后再想想,就能记住其中的绝大部分内容了。
有朋友一定很快想出一个方法:是把初始化留个C++定义文件,而不是声明文件。如:
xxx.hpp: 
class TotalTrait<float

public
 typedef double R; 
 static const R zero; 
}; 
xxx.cpp: 
 const TotalTrait<float>::R zero=0.0; 
行了,这样的确行了,但这样的分离操作容易引发一个问题,如果编译器不知道或者没法知道其他文件(.cpp)中对zero赋初值了,那么一定导致了某种未知的不确定的赋值方案。你可以试试把.Cpp中的初始化注释掉看看,如果编译器不警告或者出错,就说明zero不能按预想的方式初始化。
我们可以仔细的想,这里提供一种合适的解决方案:
template<> 
class TotalTrait<char

public
 typedef int R; 
 static R Zero(){ 
  return 0; 
 } 
}; 
template<> 
class TotalTrait<int

public
 typedef long R; 
 static R Zero(){ 
  return 0; 
 } 
}; 
class TotalTrait<float

public
 typedef double R; 
 static R Zero(){ 
  return 0.0; 
 } 
}; 
我们简单的把静态数据成员改成静态成员函数。同时对模板简单修改:
template<typename T> 
typename TotalTrait<T>::R Total(const T* beg, const T* end) 

 typedef typename TotalTrait<T>::R R; 
 R total=TotalTrait<T>::Zero(); 
 while(beg!=end) 
 { 
  total+=*beg; 
  beg++; 
 } 
 return total; 
ok,问题解决了。
一切都按照预想的进行,生活是如此的美好。人们总是在追求更美好的东西,让我们继续追寻前辈的足迹吧!
问:我们是否总是需要这种默认指定的trait呢?或者我们很多时候不需要无谓的扩大数据类型,比如很多时候我们能够保证我们的int型数组元素之和仍然在int范围内,这时候我们的返回值可以不扩大成long型。我们需要一种更灵活的配置方式,允许用户自己简单修改返回值类型。
一个有效的想法是提供模板参数:
template<typename T, typename RE> 
typename RE::R Total(const T* beg, const T* end) 

 typedef typename RE::R R; 
 R total=RE::Zero(); 
 while(beg!=end) 
 { 
  total+=*beg; 
  beg++; 
 } 
 return total; 
但这样的话,我们每次都要指定两个模板参数,实在烦人,因为多数情况下,我们不太关心返回值(只要正确就行)。默认参数,当然可以,但现行的C++标准不支持对模板函数的模板参数提供默认值。如果不能在直接的方式下解决问题,我们就应该想到用类去包装它:
template<typename T, typename RE=TotalTrait<T> > 
class CTotal 

public
 static typename RE::R Total(const T* beg, const T* end) 
 { 
  typedef typename RE::R R; 
  R total=RE::Zero(); 
  while(beg!=end) 
  { 
   total+=*beg; 
   beg++; 
  } 
  return total; 
 } 
}; 
于是我们可以这样调用:
 int sum1=CTotal<char>::Total(b, b+5);
或者:
 int sum1=CTotal<char, TotalTrait<T> >::Total(b, b+5);
这样表达式太复杂,稍显麻烦,但我们可以这样处理一下:
template<typename T> 
typename TotalTrait<T>::R Total(const T* beg, const T* end) 

 return CTotal<T>::Total(beg, end); 

 
template<typename T, typename RE> 
typename TotalTrait<T>::R Total(const T* beg, const T* end) 

 return CTotal<T, RE>::Total(beg, end); 

 
如此处理,调用形式就跟以前一样简单:
 int sum1=Total(b, b+5);
如果用户指定trait,就需要这样调用:
 int sum1=Total<char, TotalTrait<char> >(b, b+5);(所幸这种形式很少用。)
本文我们主要讨论了泛型函数中的两个问题:返回值和初始化。
最后,作为本部分的结语,让我们看看伟大的Josuttis给出的关于trait的定义:trait提供了一种配置具体元素(通常是类型)的途径,而该途径主要是用于泛型计算。