海边沫沫

相濡以沫,不如相忘于江湖
posts - 9, comments - 113, trackbacks - 0, articles - 0
  C++博客 :: 首页 :: 新随笔 :: 联系 :: 聚合  :: 管理
不知道为什么,我最近越来越觉得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++也并不是非要把这些语言的细节都啃透,除非是确实非用它不可。对于我来说,那些高质量的库,我只要会用就可以了,而且只有当确实需要的时候再去用这些库。因此,我还是保持简单的事情简单化,继续写我的简单代码吧。

Feedback

# re: Trait和Template Mataprogram的奥秘  回复  更多评论   

2008-08-31 09:37 by haskell
没啥用处,除了跟人家显摆外。
转了一圈才发现算法才是王道

# re: Trait和Template Mataprogram的奥秘  回复  更多评论   

2008-08-31 20:13 by 空明流转
...

# re: Trait和Template Mataprogram的奥秘  回复  更多评论   

2008-08-31 21:42 by wx
疑惑和你是一样的疑惑,不过还是没有弄明白

# re: Trait和Template Mataprogram的奥秘[未登录]  回复  更多评论   

2008-09-01 09:13 by Kevin Lynx
average的例子直接取得<C++ template>呀,模板递归的例子也是模板元里的常见例子。

这些东西有什么用?当你有这个思想时,你会发现它非常有用。说没用的人,那是他自己根本不懂。

# re: Trait和Template Mataprogram的奥秘  回复  更多评论   

2008-09-01 19:57 by 海边沫沫
@Kevin Lynx
你说得非常对,这几个例子都是《C++ Templates》上的,原因嘛,一是因为我觉得这些例子对于我们理解这些理论非常有帮助,二是因为以我的水平,也实在是翻不出什么花样来。

刚才去你的博客看了一下,觉得你写的那个利用宏进行代码生成的文章非常好,我想,使用模板元编程干这事,应该也是一样的吧。

我也觉得你是一个知识面非常广的实战型高手,不仅懂理论,还自己写了那么多的实现,确实令人佩服。

# re: 理解模板编程中的Trait和Mataprogram  回复  更多评论   

2008-09-08 01:03 by Bill Gates
c++ template的这些技巧把简单的思想掩盖了。其实就是代码自动生成。有2个极端,1是像lisp那样可以任意创造语法,不管类型;2就是像c++这样,完全考虑类型,结果语法怪异,难以理解。等c++0x以后可能会好点。

# re: 理解模板编程中的Trait和Mataprogram  回复  更多评论   

2008-09-11 20:24 by 海边沫沫
Bill Gates?

# re: 理解模板编程中的Trait和Mataprogram  回复  更多评论   

2009-01-05 21:44 by hycoldrain
好文

只有注册用户登录后才能发表评论。
网站导航: 博客园   IT新闻   BlogJava   知识库   博问   管理