不知道为什么,我最近越来越觉得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++也并不是非要把这些语言的细节都啃透,除非是确实非用它不可。对于我来说,那些高质量的库,我只要会用就可以了,而且只有当确实需要的时候再去用这些库。因此,我还是保持简单的事情简单化,继续写我的简单代码吧。