C++是一门复杂的语言,之所以复杂,就是因为被很多的大牛们发掘出了它的很多极其怪异的用法,或者也可以说是很高明的技巧,这些技巧,我们普通人难以掌握,也很少有非使用不可的时候,但是对于那些大牛们来说,却是必不可少的利器。不信的话,翻翻Boost的源码,这样复杂的技巧比比皆是。
这些技巧,有的我以前只是听说,比如Mataprogram;有的我曾经在使用库的时候碰到过,但是怎么也想不通为什么需要这么个东西,比如Trait。直到最近几天,读《C++ Template》的时候,才突然豁然开朗。这里写出来和大家分享。
先来说说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这几个字,模板类就有了三头六臂不成?
另外还有一个例子就是
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++为我们提供了模板元编程的能力,虽然我现在知道了它的基本实现机制,但是我依然想不到究竟什么时候需要用到模板元编程。看来我还需要读更多的书更多的文章。同时,我觉得我们还是应该保持简单的事情简单化,继续写我的简单代码吧。