从面对《Modern C++ Design》中backEnds.template的用法一无所知开始,要彻底搞清楚C++的模板是如果工作的决心其实已经埋下了。更何况今天在实现B树的时候反复被模板的编译问题折磨着。更加坚定了看完这本《C++ Template. The Complete Guide》的信念。尽管过去对这个主题知道一些东西,什么特化,偏特化,参数演绎等等。但是系统地看一看也是有必要的。当然在这里学习的时候还是非常同意Nocolai的观点,实用才是王道,因为我不是科学家也不可能成为科学家:)。
在这里学习的开始,还是乐意重复一遍那个经典的主题:Why Templates?
理由很简单,在设计数据结构或者算法的时候,需要对不同的数据类型,但是相似的行为做描述。比如说你有一个QuickSort,你想让他能对int,float, std::string都能够排序。
你有很多办法:第一就是你实现3个QuickSort, 参数分别是int[],float[],std::string[],这样是可以工作的。但是带来的问题显而易见,如果你第一次实现的int的排序是错的,那你完蛋了,你必须把这三个实现都改了。如果这个代码是你一个人维护那勉强可以,如果很多人维护,别人可不知道有这样的历史在代码里面。所以这个方案是不妥当的。
第二个方法,其实是比较常见的,就是void*或者Object来取代具体的类型,每次我实现一个容器的时候push_back的内容都是Object&或者void*,用这个Object&或者void*来代替了具体的类型。这样的好处是确实你不用重写三分代码了,但是问题一样很明显,用过了void*以后需要类型转化,原先的类型信息丢失了。在这样的情况下,一个float也被类型转化成了int加入到排序的队列中,而这确实不显示你的想法。如果Object&也一样有问题,如果待排序的数据ObjectA同时继承与ObjectB和ObjectC,而这两个父类又都是Object的子类,那样那个讨厌的菱形有出现了!!!简单的说这种对待排序参数强制的要求在维护性上是得不到很好的保证的。
当然,在模板没有应用的时候,还有人用了宏定义来取代模板。让我们来看看那些曾经熟悉的身影:
#define SWAP(a, b) { \
a ^= b; \
b ^= a; \
a ^= b; \
}
#define MAX(a, b) ((a) < (b) ? (b) : (a))
如果不是那些C/C++的大师不断强化这个MAX或者MIN的写法,我想这种"stupid replacement mechanism"肯定会让很多人抓狂。是的,这样的宏定义太容易出错了,在C++里面,他不断的被取代,简单的函数变成了inline,值define编程了enum。总之C++的基调就是忘记宏,忘记预定义。
所以在这样的时刻,在这个C#和Java都在推动template的时刻。我们还有什么理由不用C++的模板呢?
大约是受宏(预处理)的压迫太深,所以有了模板以后的第一件事情便是定义模板函数来取代那些MAX和MIN。作者也乐得如此,下面我就马上来看一个最简单的模板函数max的实现:
template <typename T>
inline T const& max (T const& a, T const& b)
{
// if a < b then use b else use a
return a < b ? b : a;
}
如果费力的逐个解释一下的话,那大概是这样:template表明了这是一个模板函数,<>指定了模板参数区域,typename表明了后面的参数是一个类型名,而T则表明了一种广泛意义上的类型,它可以用来指定所有的类型(int, float, std::string)。所有的这些构成了一个最基本的模板函数,需要说明的一点是由于历史的原因,这里的typename可以用class来取代。(我们确实看到很多都是用class来描述的)。当然也有人问了,那struct是不是也可以,答案当然是不可以!
了解了这个函数的定义,第二步要做的就是探索如何调用它。这里有几个例子:
max(1,2 ); // max<int>
max('1','2'); // max<char>
max(1, 2.5 ); // compile error
max<double>(1,2.5 ); // max<double>
第一个和第二个调用没有任何问题,就像普通的函数调用,当然他们的调用是可以成功的。但是到第三个调用的时候,问题出现了,编译器报了一个错误'const T &max(const T &,const T &)' : template parameter 'T' is ambiguous。可以看到这里的参数类型出了问题,两个参数的类型不一致。一个是int一个double,所以编译器不知道怎么指定T,注意这里编译器需要的是一个确切的类型,所以他是不会主动给你做任何类型转化的。那怕从int32到int16都不会。所以在这里你需要显示地告诉编译器你要怎么做,这就是第四个调用max<double>(1, 2.5);或者你也可以强制转化一下参数1,让他转化成double然后调用,比如说max(static_cast<double>1, 2.5)。
更进一步,我们来看看编译器是如何帮助我们实现这些调用的。简单的说,编译器采用的办法就是存在什么类型的调用,我就给你产生一堆什么类型对应的max代码。比如说max(1,2);调用来的时候,我就给你生成一段这样的代码:
inline int const& max (int const& a, int const& b)
{
return a < b ? b : a;
}
#include "stdafx.h"
#include <iostream>
#include <cstring>
#include <string>
// maximum of two values of any type
template <typename T>
inline T const& max (T const& a, T const& b)
{
return a < b ? b : a;
}
// maximum of two pointers
template <typename T>
inline T* const& max (T* const& a, T* const& b)
{
return *a < *b ? b : a;
}
// maximum of two C-strings
inline char const* const& max (char const* const& a,
char const* const& b)
{
return std::strcmp(a,b) < 0 ? b : a;
}
int _tmain(int argc, _TCHAR* argv[])
{
int a=7;
int b=42;
::max(a,b); // max() for two values of type int
std::string s="hey";
std::string t="you";
::max(s,t); // max() for two values of type std::string
int* p1 = &b;
int* p2 = &a;
::max(p1,p2); // max() for two pointers
char const* s1 = "David";
char const* s2 = "Nico";
::max(s1,s2); // max() for two C-strings
char* a1 = "CY";
char* a2 = "HY";
::max(a1,a2);
return 0;
}
这是一个完整的运行在VS2005下的例子。这里的不同就是模板函数有了不同的重载的版本,这里在实例化之前,就需要为不同的类型找到一个最合适的函数调用,比如说调用max(a,b);的时候,a,b都是int,编译器发现第一个max函数,也就是以value作为参数的版本最为匹配,所以会就此实例化出一个第一个函数的int版本,注意这里这种匹配的流程被称之为演绎(Deduction)。而演绎的基本的原则就是首先匹配并存的非模板函数,如果不成功就寻找最接近的模板函数。比如max(s1,s2);的时候,匹配到的就是const char*的那个版本的max函数,而max(a1, a2);的时候就是第二个max函数,也就是以指针作为类型参数的版本。
到目前为止还是一派鸟语花香,聪明的编译器做到我们希望的一切,max的表现和预想的出奇的一致。当然不得不提的一点就是平静之下的惊涛暗涌。且看下面的例子:
#include <iostream>
#include <cstring>
#include <string>
// maximum of two values of any type (call-by-reference)
template <typename T>
inline T const& max (T const& a, T const& b)
{
return a < b ? b : a;
}
// maximum of two C-strings (call-by-value)
inline char const* max (char const* a, char const* b)
{
return std::strcmp(a,b) < 0 ? b : a;
}
// maximum of three values of any type (call-by-reference)
template <typename T>
inline T const& max (T const& a, T const& b, T const& c)
{
return max (max(a,b), c); // error, if max(a,b) uses call-by-value
}
int main ()
{
::max(7, 42, 68); // OK
const char* s1 = "frederic";
const char* s2 = "anica";
const char* s3 = "lucas";
::max(s1, s2, s3); // ERROR
}
在粗粗地说了Function Templates之后,肯定要说说最主要的的Class Templates。因为是开始,所以肯定要说的明白点,清楚点:
#include <vector>
#include <stdexcept>
template <typename T>
class Stack {
private:
std::vector<T> elems; // elements
public:
void push(T const&); // push element
void pop(); // pop element
T top() const; // return top element
bool empty() const { // return whether the stack is empty
return elems.empty();
}
};
template <typename T>
void Stack<T>::push (T const& elem)
{
elems.push_back(elem); // append copy of passed elem
}
template<typename T>
void Stack<T>::pop ()
{
if (elems.empty()) {
throw std::out_of_range("Stack<>::pop(): empty stack");
}
elems.pop_back(); // remove last element
}
template <typename T>
T Stack<T>::top () const
{
if (elems.empty()) {
throw std::out_of_range("Stack<>::top(): empty stack");
}
return elems.back(); // return copy of last element
}
上面的这个呢就是作者给出的一个最基本的模板类的实现。和模板函数一样,可以看到熟悉的template关键字,<typename T>模板参数的类型标识符。这个实现的好处在于,不仅告诉了你如果申明一个模板类,同时还有如果实现这个模板类的接口。我们可以看到,除了这个Stack<T>::这个类型标识符在这里以外,他和一个模板函数是一样的。。通过这个例子可以发现,如果使用模板类也在其中了,std::vector<T> elems;所以如果我们一旦要使用这个Stack类的话,一样的,也可以用Stack<T> elem;来申明,当然T可以是int, float, std::string...如果是函数调用,那和普通类的调用一模一样。可以说这个实现还是波澜不惊的。
值得注意的一点是我们常常在成熟的类库中看到模板参数也是模板类的定义,例如:typedef Functor<Product*,Seq<int,int> > CreateFunctor;这个东西看似平淡,无非是这个Functor的第二个模板参数是Seq<int,int>而已,但是实际上,这个int>之后必须要有一个空格!没有这个空格,那连续两个大于号>>就被编译器误以为是>>操作符。但是,其实重点说的是但是!!!在VS2005中,编译器已经接受这样的定义,即使没有中间的空格,编译器还是可以正确理解的,只是如果你想写个在各个编译器下都可以跑的代码的话,还是建议你中间加上空格。 (编译器总是在改变,书却没有改变,跟不上时代也是很正常的事情 ^_^)
一般都是这样,在没有模板的时候,我们都期望有这样一个普遍成立的东西可以减少我们的工作,可真有了这个东西以后呢.又发现了这样的代码是针对所有的类型的,如果我需要对某些类型做定制呢.这个也有办法,这个办法就被称之为特化(Specialization).这里有一个Stack类为std::string做特化的例子.
#include "stack1.hpp"
template<>
class Stack<std::string> {
private:
std::deque<std::string> elems; // elements
public:
void push(std::string const&); // push element
void pop(); // pop element
std::string top() const; // return top element
bool empty() const { // return whether the stack is empty
return elems.empty();
}
};
void Stack<std::string>::push (std::string const& elem)
{
elems.push_back(elem); // append copy of passed elem
}
void Stack<std::string>::pop ()
{
if (elems.empty()) {
throw std::out_of_range
("Stack<std::string>::pop(): empty stack");
}
elems.pop_back(); // remove last element
}
std::string Stack<std::string>::top () const
{
if (elems.empty()) {
throw std::out_of_range
("Stack<std::string>::top(): empty stack");
}
return elems.back(); // return copy of last element
}
在这里用template<>class Stack<std::string> 这样一个头来表示对Stack类进行std::string类型的特化.在原文中有一句话,说的是如果你要特化一个类,那你必须特化所有的函数.当然,必须说明的是,必须特化你会用到的函数,如果这个top的函数以后不会使用.编译器也不会说你特化的不完整.其实如果你需要特化的范围比较小的话,直接特化一个函数也是可以的.比如你可以选择写成这样template<> void Stack<std::string>pop();注意,这里的template<>不写也是可以的.
到现在为止,我们见到的模板类的模板参数都只有一个, 当然我们可以有多个模板参数.这里先来看个刺激的:)
template
<
class AbstractProduct,
typename IdentifierType,
typename CreatorParmTList = NullType,
template<class> class EncapsulationPolicy = SimplePointer,
class CreationPolicy = AlwaysCreate,
template <typename , typename> class EvictionPolicy = EvictRandom,
class StatisticPolicy = NoStatisticPolicy,
template<typename, class> class FactoryErrorPolicy = DefaultFactoryError,
class ObjVector = std::vector<AbstractProduct*>
>
class CachedFactory :
protected EncapsulationPolicy<AbstractProduct>,
public CreationPolicy, public StatisticPolicy, EvictionPolicy< AbstractProduct * , unsigned >
这个是Loki里面一个寻常的定义,拿出来是想说明类模板是可以有多个模板参数的,同时就像普通的函数定义一样,也可以有默认参数.可能这个例子不是很合适,应该说这里不仅可以有参数,而且可以没有类型的模板参数.就是不是一个typename T类型的参数,比如说可以有个int的参数.像这样的定义也是可以的:template<typename T, int size>class Stack;这个size这里就可以留给 Stack类的使用者来决定(关键是编译器决定)这个Stack的大小. 当然了, int可以那short可以吗, long可以吗?原则上整数类型都是可以的,但是Float和Double直至今日还是不可以的.
回到特化上的事情上来,现在有了这么多参数,你还要特化岂不是很费力?没关系,C++ template还提供一个特性所谓偏特化( Partial Specialization)就是你可以针对一个或者几个模板参数进行特化而不是所有的参数.请原谅我的懒惰这就是一个偏特化的举例:
template<class T, class T2, class T3>class Stack
{
public:
void f1()
{
}
};
template<class T> class Stack<T, int, long>
{
public:
void f1()
{
}
};
当然啦,其实和特化差不多, 只不多是有选择性的而已.
好了,今天也差不多就说这么多吧,现在这个书看起来还是平平淡淡的,还看不出波澜.但是在深入的过程中还是慢慢会展现他的深邃,这个到和<<Modern C++ Design>>完全不同,不过也别有一番风味.
转自:http://blog.csdn.net/hhygcy/archive/2009/02/20/3914287.aspx