理解模板编程中的Trait和Mataprogram

不知道为什么,我最近越来越觉得C++太难了,也许是因为我也陷入到扣语言细节的泥沼了吧。不过换个角度来讲,C++之所以这么有吸引力,也多亏了它有这么多复杂的细节,正是因为这些细节不断被发掘,才更加引得C++程序员激情四射、奋不顾身。比如说模板元编程,C++模板在设计之初根本没有想到模板元编程这回事,更没想到C++模板系统是图灵完备的,结果1994年Erwin Unruh提数了可以使用模板在编译器进行某些计算后,无数的大牛人便前仆后继,将模板编程发挥到了极致。

我想我也是属于那种没事找抽的人,要不然我为什么会抱着《C++ Templates》这本书看呢?奈何我能力有限,兼且经验不足,使用C++的时候少,使用模板的时候更少,所以对于书中的内容,要么就是看了不是很懂,要么就是看了也不知道它有什么用。但是也不是完全没有收获,对于以前两个百思不得其解的问题,也还算是灵光一闪、豁然贯通了。

其中一个是Trait,这是我以前在使用STL和ATL库的时候遇到过的,虽然想不透其中的奥妙,但是不影响我写程序。另外一个是模板元编程,只听说过,如雷灌耳,但是却从来没有见过,也想象不出它的原理,《C++ Templates》终于让我看到了它的庐山真面目。

先来说说Trait,这是一个在C++ Template编程中经常用到的一个设计机制,我在使用STL库中的basic_string时见到过,其定义如下:
template  <
   
class  CharType,
   
class  Traits = char_traits < CharType >
   
class  Allocator = allocator < CharType >  
>
class  basic_string


其中就有一个模板参数为Traits,而它的默认值为char_traits<CharType>,这里的char_traits<>就是一个trait类,它可以提供关于CharType的特征信息。我们常用的string类的定义如下:
typedef basic_string < char >   string

如果我们把它的默认模板参数带入,就可以看到string的形式是这样的:
basic_string <   char , char_traits < char > , allocator < char >   >


到这里,我就迷糊了,我在想,为什么char_trait<>就能够取得char的类型信息?为什么basic_string<>就不行?难道说加上trait这几个字,模板类就有了三头六臂不成?

另外一个见到Trait的地方就是 ATL 3.0中的窗口类,这是我很早以前翻译的一篇文章,其中也使用到了Trait,在定义窗口样式的时候,其代码如下:
class  CMyWindow:  public  CWindowImpl <
   CMyWindow,
   CWindow,
   CWinTraits
< WS_OVERLAPPEDWINDOW | WS_VISIBLE, 0 >  
>
{};

当时我就想了,为什么不直接把“ WS_OVERLAPPEDWINDOW | WS_VISIBLE, 0”当成模板参数传递给CWindowImpl<>算了,还非要CWinTraits<>来掺和一把?

直到现在,我终于知道,原来一直错的就是我。我不该把char_traits<>看成是一个模板类,不该认为传给它一个char它就可以读出char的特征信息,传给它一个int它就能读出int的特征信息。它当然不可能具备这么高级的功能,更不可能加上traits几个字就一下子挣脱了C++语言的束缚。

那么不把它看成一个模板类,应该怎么看呢?应该把char_traits<char>看成一个整体,说专业点,那叫模板特化,说通俗点,就是原来这里面的特征信息都是编写它的人自己定义的,如果你要让basic_string能够处理int,double之类的信息,你还得自己写一个char_traits<int>和一个char_traits<double>。CWinTraits<...>也同样是这个道理。

为了说得更清楚点,我这里举个小例子。什么例子呢?就写个计算平均值的模板函数吧,如下:
template  < typename T >
T average(T 
const *  begin, T  const *  end)
{
    T total 
=  T();
    
int  count  =   0 ;
    
while  (begin  !=  end){
        total 
+=   *  begin;
        
++ begin;
        
++ count;
    }
    
return  total / count;
}

下面是使用这个函数的代码,如果我们计算的类型是int,结果是正确的,如下:
int  main(){
    
int  numbers[]  =  { 1 , 2 , 3 , 4 , 5 };
    std::cout 
<<  average( & numbers[ 0 ], & numbers[ 5 ])  <<  std::endl;
}

该程序运行的结果是3,非常正确,将数据类型换成float,double也没有问题。但是,如果是char类型,就不一定了。代码如下:
int  main(){
    
char  characters[]  =   " traits " ;
    std::cout 
<<  static_cast < int > (average( & characters[ 0 ], & characters[ 6 ]))  <<  std::endl;
}


运行结果为 -17,不信大家可以自己运行试一下。为什么是个负数呢?

原因是因为char类型能表示的范围只有-127到+128,几个字母一加,就溢出了。为了得到正确的结果,我们希望能有一种机制,来指定运算的时候用什么作为返回类型,这时候,traits就可以闪亮登场了。前面已经说过,要把trait<...>看成一个整体,所以应该为每一个数据类型都定义一个trait。在这个例子中,我们主要是为了对每一个运算的类型指定合适的返回类型,任务比较简单,所以,代码可以这样写:

template  < typename T >
class  TypeTraits;

template 
<>
class  TypeTraits < char > {
public :
    typedef 
int  ReturnType;
}

template 
<>
class  TypeTraits < short > {
public :
    typedef 
int  ReturnType;
}

template 
<>
class  TypeTraits < int > {
public :
    typedef 
int  ReturnType;
}

template 
<>
class  TypeTraits < float > {
public :
    typedef 
double  ReturnType;
}

函数可以改成这样:
template  < typename T,typename Traits >
typename Traits::ReturnType average(T 
const *  begin, T  const *  end)
{
    typedef typename Traits::ReturnType ReturnType;
    ReturnType total 
=  ReturnType();
    
int  count  =   0 ;
    
while  (begin  !=  end){
        total 
+=   *  begin;
        
++ begin;
        
++ count;
    }
    
return  total / count;
}

使用该函数的代码是这样:
int  main(){
    
int  numbers[]  =  { 1 , 2 , 3 , 4 , 5 };
    std::cout 
<<  average < int ,TypeTraits < int >   > ( & numbers[ 0 ], & numbers[ 5 ])  <<  std::endl;
    
char  characters[]  =   " traits " ;
    std::cout 
<<  average < char ,TypeTraits < char >   > ( & characters[ 0 ], & characters[ 6 ])  <<  std::endl;
}

这时候,一切都正常了。只可惜模板函数不支持默认模板参数,要不然,这里的代码可以更简洁。

再来说说Template Mataprogram,中文叫模板元编程。我之能听说它,并对它不甚向往,主要是因为它有这样几个特点:
1、它编的程序不是运行的时候执行的,而是在编译的时候由编译器执行的;
2、它能够牵着编译器的鼻子走,靠的完全是符合标准的模板语法,不需要使用编译器的任何API;
3、它居然是图灵完备的,也就是说它什么事都能干。

牛吧?C++提供了一个模板机制,这些大牛们居然可以用模板把编译器耍得团团转,居然能在程序还没运行的时候就什么都能干。反正我是崇拜得五体投地。直到最近看书,才找到了它的奥秘所在,当然了,只限于基本原理。

那么,这个基本原理是怎样的呢?其实就是靠的模板的实例化,和使用枚举值或静态常量。具体来说是这样:当编译器遇到enum的定义的时候,就会对该enum进行求值,这个求值是在编译期进行的,而如果该enum对应的表达式是一个模板类的成员,则会实例化该模板类,而实例化模板类的时候,又是递归进行的,这样,就可以在递归的过程中作我们想做的任何事(理论上可以做任何事,但是以我的水平,也就只能算算加减乘除)。看起来是不是不好理解?没关系,下面看一个例子,计算N的阶乘:
template  < int  N >
class  Factorial
{
public :
    
enum  { result  =  N  *  Factorial < N - 1 > ::result };
};

这下该明白了吧,为了得到Factorial<N>::result的值,就会实例化Factorial<N>,然后又会实例化Factorial<N-1>,依次类推,一直递归下去。那么什么时候结束呢?所以还需要一个特化版本:
template <>
class  Factorial < 1 >
{
public :
    
enum  { result  =   1  };
}

下面写几行代码测试一下,如下:
int  main()
{
    std::cout 
<<  Factorial < 10 > ::result  <<  std::endl;
    
return   0 ;
}

OK,事情就这么简单。大家都知道,递归可以代替循环,就只是对内存的消耗大一些,所以递归的层次不能太多。解决了循环的问题,那么分支结构如何解决呢?

不用担心,看看下面这样的模板定义:
template  < bool  C, typename Ta, typename Tb >
class  IfThenElse;

template 
< typename Ta, typename Tb >
class  IfThenElse < true , Ta, Tb > {
public :
    typedef Ta ResultT;
};

template 
< typename Ta, typename Tb >
class  IfThenElse < false , Ta, Tb > {
public :
    typedef Tb ResultT;
};

一个模板类加上两个局部特化版本就解决了问题,如果第一个模板参数是true,则选择Ta作为结果,否则就选择Tb作为结果。

虽然C++为我们提供了模板元编程的能力,虽然我现在知道了它的基本实现机制,但是我依然想不到究竟什么时候需要用到模板元编程,听说要开发高可用性的第三方库少了它不行,也听说Boost库中到处可以见到它的身影,但仅仅只是听说而已,我自己是想不到,也做不到。

当然了,学习C++也并不是非要把这些语言的细节都啃透,除非是确实非用它不可。对于我来说,那些高质量的库,我只要会用就可以了,而且只有当确实需要的时候再去用这些库。因此,我还是保持简单的事情简单化,继续写我的简单代码吧。

你可能感兴趣的:(理解模板编程中的Trait和Mataprogram)