ANSI/ISO C++ Professional Programmer's Handbook(9)

 摘自: http://sttony.blogspot.com/search/label/C%2B%2B

9


Templates


by Danny Kalev




简介


模板是一种编译器产生类或函数体的模式。自从在1991年第一次实现模板以来(cfront2.0),C++的程序设计和概念有了相当大的变化。最初模板被视为对如ArrayList这种泛型容器类的一种支持。近几年,使用模板的经验显示,这种特性对于设计和实现通用库十分有用,比如STL。模板化的库和frameworks都是高效方便的。通过模板的广泛使用,C++增加了许多复杂高级的结构来控制模板的行为何性能,包括部分特定化、显式特定化、模板成员、引出模板、默认参数类型等等。


本章讨论了设计和实现模板的各个的方面。首先解释类模板。接着是函数模板。最后,讨论一些被特别关注的模板问题——比如指向成员的指针、类模板的虚成员函数、继承关系和显式实例化。


类模板


许多算法和结构可以独立于他们所操作数据的类型。一般的,对数据类型的依赖性仅仅是人造的产物。例如,复数的概念并不排外的限制与基本类型double。相反,它应该能接受任何浮点类型。设计不依赖类型的抽象复数类游很多优点,因为它使用户能够为特定的应用程序选择所需的精度,而不需要手工重复代码。另外,不依赖类型的类在不同平台之间更容易转换。


类模板的申明


使用关键字template来申明类模板,template之后是模板参数列表(template parameter list),用尖括号括起来,之后是类申明或类定义。例如



template <class T> class Vector; //申明
template <class T> class Vector //定义
{
private:
size_t sz;
T * buff;
public:
explicit Vector<T>(size_t s = 100);
Vector<T> (const Vector <T> & v); //拷贝构造器
Vector<T>& operator= (const Vector<T>& v); //赋值运算符
~Vector<T>(); //销毁器
//其他成员函数
T& operator [] (unsigned int index);
const T& operator [] (unsigned int index) const;
size_t size() const;
};

类模板的成员函数可以在类体外面定义。这是,他们必须显式申明为他们类模板的成员函数。例如



//定义Vector的成员函数
//之后是类申明
template <class T> Vector<T>::Vector<T> (size_t s) //构造器定义
: sz(s),
buff (new T[s])
{}
template <class T> Vector<T>::Vector<T> (const Vector <T> & v) //拷贝构造器
{
sz = 0;
buff = 0;
*this = v; //使用重载赋值运算符
}
template <class T> Vector<T>& Vector<T>::operator= //赋值运算符
(const Vector <T> & v)
{
if (this == &v)
return *this;
this->Vector<T>::~Vector<T>(); //调用销毁器
buff = new T[v.size()]; //分配足够的存储空间
for (size_t i =0; i < v.size(); i++)
buff[i] = v[i]; //成员分别拷贝
sz = v.size();
return *this;
}
template <class T> Vector<T>::~Vector<T> () //销毁器
{
delete [] buff;
}
template <class T> inline T& Vector<T>::operator [] (unsigned int i)
{
return buff[i];
}
template <class T> inline const T& Vector<T>::operator [] //常量版本
(unsigned int i) const
{
return buff[i];
}
template <class T> inline size_t Vector<T>::size () const
{
return sz;
}
//Vector.hpp

前缀template <class T>指示T是一个模板参数(template parameter),它是未指定类型的占位符。关键字class应该特别关注,因为形参T的实参可以不是用户定义类型;也可以是charint这样的基本类型。如果你喜欢更不确定的含义,也可以使用关键字typename来代替class(当然稍后你就会看到typename夜有其他用途):



template <typename T> class Vector //用typename代替class
//两种形式没有语义上的差别
{
//...
};
template <typename T> Vector<T>::Vector<T> (size_t s)
: sz(s), buff (new T[s]) {}

Vector地范围内,参数T不需要加上范围限制,所以不用它也可以定义申明成员函数。例如构造器可以有如下申明:



template <class T> class Vector
{
public:
Vector (size_t s = 100); //等价与Vector <T>(size_t s = 100);
};

同样的,构造器的定义可以省略多余的形参:



//等价与template <class T> Vector<T>::Vector<T>(size_t s)
template <class T> Vector<T>::Vector (size_t s) :
buff (new T[s]), sz(s)
{}

实例化和特殊化


类模板不是类。从类模板和类型形参实例化类的过程叫模板实例化(template instantiation)模板 id,就是模板名字和紧接其后尖括号括起来的形参列表,叫特殊化(specialization)。类模板的特殊化可以同类一样使用。考虑下面的例子:



void func (Vector <float> &); //函数实参
size_t n = sizeof( Vector <char>); //sizeof表达式
class myStringVector: private Vector<std::string> //基类
{/*...*/};
#include <iostream>
#include <typeinfo>
#include <string>
using namespace std;
cout<<typeid(Vector< string>).name(); //typeid表达式
Vector<int> vi; //创建对象

编译器只实例化给出特殊化的成员函数。在下面的例子中,统计了成员实例化的数目:



#include <iostream>
#include "Vector.hpp"
using namespace std;
int main()
{
Vector<int> vi(5); // 1
for (int i = 0; i<5; i++)
{
vi[i] = i; //fill vi // 2
cout<<vi[i]<<endl;
}
return 0; // 3
}

编译器只为显式或隐含在程序中使用过的Vector成员函数产生代码::



Vector<int>::Vector<int> (size_t s) //1: 构造器
: sz(s), buff (new int[s]) {}
inline int& Vector<int>::operator [] (unsigned int idx) //2: 运算符 []
{
return buff[idx];
}
Vector<int>::~Vector<int> () //3: 销毁器
{
delete [] buff;
}

相反的,编译器没有产生成员函数size_t Vector<int>::size() const的代码,因为并不需要。对于有些编译器,可能简单的一次产生所有成员函数的代码,不管需不需要。但是,“需要才产生(generate on demand)”方针是标准所要求的,有两种功能:




  • 效率——定义了成百个成员函数的类模板也很常见(例如STL容器);而实际上在程序中使用的不到一打。为在程序中类有特殊化而没有使用过的成员函数产生代码,可以轻易的使科执行代码膨胀——而且增加了不必要的编译和链接时间。





  • 灵活性——有些情况下,并不是所有的类型都支持类模板中定义的所有运算。例如,容器类可能使用ostream的运算符<<来显示像charint这样的基本类型以及重载了<<运算符的用户定义类型。然而,没有重载<<的POD(无格式)结构也可能被存储在容器中,而没有调用<<





模板形参


模板可以使用类型实参,就是表示还没有指定的类型的符号。例如



template <class T > class Vector {/*...*/};

模板也可以接受普通类型如intlong作为实参:



template <class T, int n> class Array
{
private:
T a[n];
int size;
public:
Array() : size (n){}
T& operator [] (int idx) { return a[idx]; }
};

但是注意,当普通类型作为形参时,整数类型的模板形参必须是常量或常量表达式。例如



void array_user()
{
const int cn = 5;
int num = 10;
Array<char, 5> ac; // OK, 5是常量
Array<float, cn> af; // OK, cn是常量
Array<unsigned char, sizeof(float)> auc; // OK, 常量表达式
Array<bool, num> ab; // 错误,num不是常量
}

除了常量表达式以外,其他允许的形参有非重载的指向成员的指针和对象或外部链接函数的地址。这也意味着显式字符串不能用于模板形参,因为它是内部链接。例如:



template <class T, const char *> class A
{/*...*/};
void array_user()
{
const char * p ="illegal";
A<int, "invalid"> aa; // 错误,显式字符串用于模板形参
A<int, p> ab; //也是错误的,p没有外部链接
}

模板也可以接受模板作为形参。例如



int send(const Vector<char*>& );
int main()
{
Vector <Vector<char*> > msg_que(10); //模板用于形参
//...填充 msg_que
for (int i =0; i < 10; i++) //发送消息
send(msg_que[i]);
return 0;
}

注意模板作为形参时,尖括号间的空格是必需的:



Vector <Vector<char*> > msg_que(10);

否则,两个>>与右移运算符混淆。


一个typedef可以避免这种麻烦并且使编译器和其他程序员的更好读懂:



typedef Vector<char *> msg;
Vector<msg> msg_que;

默认类型参数


类模板可以有默认参数。


就像函数的默认参数一样,模板的默认类型参数给了程序员为特定应用程序选择最佳类型的灵活性。例如 ,Vector模板可以对特定内存管理任务进行优化。不使用硬编码的size_t类型来存储Vector大小,可以将大小类型作为一个参数。对于大多数用途,使用默认类型size_t。但是。对于管理非常大的内存缓冲——或非常小的——程序员可以自由选择适当地数据类型代替。例如



template <class T, class S = size_t > class Vector
{
private:
S sz;
T * buff;
public:
explicit Vector(S s = 100): sz(s), buff(new T[s]){}
~Vector();
//其他成员函数
S size() const;
};
template <class T, class S> Vector<T,S>::~Vector<T,S>()//销毁器定义
{
delete [] buff;
}
template <class T, class S> S Vector<T,S>::size() const
{
return sz;
}
int main()
{
Vector <int> ordinary;
Vector <int, unsigned char> tiny(5);
return 0;
}

默认大小类型的另一个优点是支持有具体编译器决定的类型。在支持64位整数的机器,程序员可以使用Vector来简单的操作非常大的内存缓冲区:



Vector <int, unsigned __int64> very_huge;

不需要过分强调这个事实,程序员不必要更改Vector的定义就可以恰当地特殊化。如果没有模板,像这样高级的自动化很难达到。


静态数据成员


模板可以有静态数据成员。例如



template<class T> class C
{
public:
static T stat;
};
template<class T> T C<T>::stat = 5; //定义

静态数据成员可以这样访问:



void f()
{
int n = C<int>::stat;
}

友元


类模板的友元可以是函数模板或类模板,特殊化的函数模板和类模板或是普通(非模板的)函数或类。下面的章节讨论了类模板可以有的各种友元。


非模板友元

模板类的非模板友元申明和非模板类的友元申明十分相似。在下面的例子中,类模板Vector申明普通函数f()和普通类Thing作为它的友元:



class Thing;
template <class T> class Vector
{
public:
//...
friend void f ();
friend class Thing;
};

每一个Vector的特殊化都有友元:函数f()和类Thing


特殊化

你还可以申明特殊化模板作为类模板的友元。在下面的例子中,类模板Vector申明特殊化C<void*>作为自己的友元:



template <class T> class C{/*...*/};
template <class T> class Vector
{
public:
//...
friend class C<void*>; //其他特殊化不是Vector的友元
};

模板友元

类模板的友元类可以是模板。例如,你可以申明磊模板作为其他类模板的友元:



template <class U> class D{/*...*/};
template <class T> class Vector
{
public:
//...
template <class U> friend class D;
};

每一个D的特殊化是每一个Vector特殊化的友元。你也可以申明函数模板作为友元(函数模板将在稍后详细讨论)。例如,你可以增加一个重载运算符==的函数模板来测试两个Vector对象的相等性。从而对于每一个Vector的特殊化,编译器将产生相应重载运算符==特殊化的代码。为了申明一个友元模板函数,你必须先申明类模板和友元函数模板:



template <class T> class Vector; // 先申明类模板
//作为友元使用之前先申明函数模板
template <class T> bool operator== (const Vector<T>& v1, const Vector<T>& v2);

然后在类体中申明友元函数模板:




template <class T> class Vector
{
public:
//...
friend bool operator==<T> (const Vector<T>& v1, const Vector<T>& v2);
};

最后定义友元函数模板:



template <class T> bool operator== (const Vector<T>& v1, const Vector<T>& v2)
{
// 两个Vectors当且仅当:
// 1) 他们有相同的数量的元素;
// 2) 每一个元素都必须相同
if (v1.size() != v2.size())
return false;
for (size_t i = 0; i<v1.size(); i++)
{
if(v1[i] != v2[i])
return false;
}
return true;
}

部分特殊化


可以定义一个类模板的部分特殊化(partial specialization)。部分特殊化提供一种定义原始模板(primary template)的选择。在任一个特殊化的实参与在部分特殊化中给出的参数相匹配时,部分特殊化用来代替原始定义。例如,Vector的部分特殊化可以专门处理指针类型。因此,对于基本类型和用户定义类型,使用原始Vector类。对于指针,部分特殊化用来代替原始的类模板定义。一个指针部分特殊化可以在许多方面使对指针的操作最优化。


另外,解除指针的引用和->运算符,其他类型是不会用的。


部分特殊化定义为:



//filename: Vector.cpp
template <class T> class Vector <T*> //Vector <T>的部分特殊化
{
private:
size_t size;
void * p;
public:
Vector();
~Vector();
//...成员函数
size_t size() const;
};
//Vector.cpp

部分特殊化通过类模板名字后面的形参列表来指定(记住原始类模板申明时名字后面没有表)。比较两种形式:



template <class T> class Vector //原始模板
{};
template <class T> class Vector <T*> //部分特殊化
{};

有多个形参的部分特殊化类模板申明为:



template<class T, class U, int i> class A { }; // 原始
template<class T, int i> class A<T, T*, i> { }; // 部分特殊化
template<class T> class A<int, T*, 8> { }; // 其他部分特殊化
部分特殊化必须在原始类模板申明之后出现,而且其形参不能包括默认类型

类模板的显式特殊化


类模板的显式特殊化(explicit specialization)提供了原始模板定义的选择。如果部分特殊化中的实参与显式特殊化中给出的相匹配时,显式特殊化用来代替原始定义。它在什么时候有用呢?考虑模板Vector:编译器为特殊化Vector<bool>生成的代码是十分低效。并不是用一位来存储每一个Boolean值,它至少用一个字节。当你操作大量比特时,比如逻辑运算和数字信号处理,这将是不可忍受的。另外,用位运算符来进行面向位的操作有更高的效率。显然,为处理比特经过特殊调整的Vector模板有重要的优点。下面是为了用比特而不是字节来处理bool的Vector<bool>显式特殊化的例子:



template <> class Vector <bool> //显式特殊化
{
private:
size_t sz;
unsigned char * buff;
public:
explicit Vector(size_t s = 1) : sz(s),
buff (new unsigned char [(sz+7U)/8U] ) {}
Vector<bool> (const Vector <bool> & v);
Vector<bool>& operator= (const Vector<bool>& v);
~Vector<bool>();
//其他成员函数
bool& operator [] (unsigned int index);
const bool& operator [] (unsigned int index) const;
size_t size() const;
};
void bitmanip()
{
Vector< bool> bits(8);
bits[0] = true; //赋值assign
bool seventh = bits[7]; //检索
}

template<>前缀指示这是原始模板的显式特殊化。特殊化的模板实参在尖括号种马上指定了。迄今已定义的Vector的特殊化体系如下:



template <class T> class Vector //原始模板
{};
template <class T> class Vector <T*> //部分特殊化
{};
template <> class Vector <bool> //显式特殊化
{};

幸运的是,STL已经定义了std::vector<bool>的特殊化来优化对比特的处理,你将在下一章“STL和泛型程序设计”中读到。


类模板函数的特殊化


Vector重载运算符==执行可能的在两个Vector对象之间的比较,当两个对象的元素都是基本类型或是重载了运算符==的对象时。但是比较存储C字符串的对象时可能会产生错误。例如



#include "Vector.hpp"
extern const char msg1[] = "hello";
extern const char msg2[] = "hello";
int main()
{
Vector<const char *> v1(1), v2(1); //元素数量相同
v1[0] = msg1;
v2[0] = msg2;
bool equal = (v1 == v2); //错误,字符串相同但指针不同
return 0;
}




注意:字面上一致的字符串是否被当作不同对象依赖于具体编译器的实现。有些编译器可能将常量 msg1msg2存储在同一个地址(在这样的编译器上表达式 bool equal = (v1 == v2);是true)。但是这里假定 msg1msg2存储在两个不同的内存地址。


尽管v1v2有相同数量的元素而且其元素存储的相同的字符串值,运算符==返回false,因为它比较字符串的地址而不是它们的内容。你可以专门为const char *类型定义特殊版本的运算符==,来比较字符串的值而不是地址。当类型Vector<const char *>进行比较时,编译器只选择特定的版本。否则就使用原始版本的运算符==。没有必要在类模板Vector中增加一种特殊的友元运算符==。但是仍然推荐你这么做,以标出特殊运算符==的存在。例如



template <class T> class Vector;
template <class T> bool operator== (const Vector<T>& v1, const Vector<T>& v2);
template <class T> class Vector
{
//...
public:
friend bool operator==<T> (const Vector<T>& v1,
const Vector<T>& v2); //原始
friend bool operator== ( //特殊版本
const Vector<const char *>& v1,
const Vector<const char *>& v2);
};

特殊版本的定义必须出现在一般版本之后。所以,你可以将它放到一个头文件中,已确保在一般版本之后。下面是特殊版本的例子:



//appended to vector.cpp
#include <cstring> //需要strcmp
using namespace std;
template <> bool operator== (
const Vector<const char *>& v1,
const Vector<const char *>& v2 )
{
if (v1.size() != v2.size()) //与以前一样
return false;
for (size_t i = 0; i<v1.size(); i++)
{
if (strcmp(v1[i], v2[i]) != 0) //比较字符串的值
return false;
}
return true;
}

这里也一样,关键字template后面的空尖括号指定特殊版本来代替以前定义的一般版本。编译器现在用特殊形式的运算符==来比较v1v2;如期望的,现在结果是true


特殊函数以类似于派生类虚成员函数的行为来处理。两种情况下,实际的被调用的函数确定于类型。只不过,虚函数机制依赖对象的动态类型,而函数特殊化是静态的。


函数模板


许多算法执行一系列相同的操作,而不用管所操作数据的类型。minmaxarray sortswap都是这种不依赖类型的例子。在没有模板的时候,程序员不得不使用其他技术来实现泛型算法:宏,void指针和公共根基类——所有这些都有明显的缺点。本节先举例说明这些技术的缺点,再示范如何在泛型程序设计中使用函数模板。


带参数的宏


带参数宏是在C中实现泛型算法的主要形式。例如



#define min(x,y) ((x)<(y))?(x):(y)
void f()
{
double dlower = min(5.5, 5.4);
int ilower = min(sizeof(double), sizeof(int));
char clower = min('a', 'b');
}

标准C库库一了各种带参数宏。在一定的范围内,使用他们很有效率,因为避免了完全函数调用的开销。但是宏也有明显得缺点。预处理程序对宏指示做简单的文本替换,只有非常有限的范围限制和类型检查。而且宏的难以调试是臭名昭著的,因为编译器扫描可能与看上去与原来的源文件区别很大的源文件。由于这个原因,编译器可能指出原来的源文件不包括的代码。宏可以轻易的膨胀程序的尺寸,因为每一个宏调用都被展开了。当复杂的宏被反复调用时,导致程序的尺寸意想不到的膨胀。尽管语法类似,宏与函数在语义上有很大差别——宏没有连接、地址和存储类型。由于这些原因,谨慎地是使用宏——或责不用。


void指针


近似于宏的选择是使用泛型指针void *,它可以存储任何类型的地址。C标准库定义的两个泛型函数使用了这种特性qsortbsearchqsort在头文件<stdlib.h>中有如下定义:



void qsort( void *,
size_t,
size_t,
int (*) (const void *, const void *)
);

这种泛型函数的类型无关性通过使用void指针和一个抽象的用户定义比较函数来实现。尽管如此,这种技术还是有值得注意的缺点。void指针不是类型安全的,而且反复的函数回调引起了远行其开销,在大多数情况下应该用内联来避免。


公共根基类


在一些面向对象语言中,每一个对象都是从一个公共基类类派生(在C++中这种设计的方法和缺点在第五章“面向对象的编程和设计”中详细讨论)。泛型算法可以依赖这种特性。例如



// 伪 C++ code
class Object //公共根类
{
public:
virtual bool operator < (const Object&) const; //多态行为
//..其他成员函数
};
const Object& min(const Object &x, const Object& y)
{
return x.operator<(y) ? x : y; //x和y可以使任何类的对象
}

可是在C++中模仿这种方法并不象在其他语言中那么有用。C++并不强制的有一个公共根基类。因此,这是程序员的——而不是编译器的——责任来保证每一个类都要从公共基类派生。更糟糕的是,公共根基类并不是标准的。结果这样的算法并不是通用的。另外这些算法不能处理基本类型,因为基本类型不是类对象。最后,对于这种算法,随着RTTI的广泛使用将带来不可接受的性能损失。



函数模板没有以上所有缺点。他们是类型安全的,他们可以是内联函数以提高性能,而且——最重要的——他们可以一致的对待基本类型和用户定义类型。


函数模板申明包括关键字template,之后是模板参数列表和函数申明。与普通函数不一样的,普通函数地申明和定义通常不再一个translation unit中,而模板的定义紧跟着其申明。例如



template <class T> T max( T t1, T t2)
{
return (t1 > t2) ? t1 : t2;
}

与类模板不同,函数模板的形参可以由实参的类型隐含的推出。


在下面的例子中,编译器实例化了三种不同的swap的特殊化,考虑每一个调用中使用实参的类型:



#include <string>
using namespace std;
int main()
{
int i = 0, j = 8;
char c = 'a', d = 'z';
string s1 = "first", s2 = "second";
int nmax = max(i, j); // int max (int, int);
char cmax = max(c, d); // char max (char, char);
string smax = max(s1, s2); // string max (string, string);
return 0;
}

可以使用同样的名字定义几个函数模板(重载)或也可以定义与普通函数同名的函数模板。例如



template <class T> T max( T t1, T t2)
{
return (t1 > t2) ? t1 : t2;
}
int max (int i, int j)
{
return (i > j) ? i : j;
}

编译器不会产生max()int版本的代码。在用类型为int的实参调用调用max()时,编译器调用函数int max (int, int)


性能的考虑


C++提供许多工具来控制模板的实例化,包括模板的显式实例化和引出模板。下面的章节示范怎样使用这些特性,以及其他提高性能的技术。


等价类型


当下列条件都成立时,两个模板是等价的(就是说,他们指的同一个模板id):




  • 名字相同——模板的名字相同并且指的是相同的模板。





  • 参数相同——模板的参数类型是相同的。





  • 同样的非类型参数——两个模板非类型的整数或枚举类型参数有相同的值,并且他们的非类型指针参数引用的是相同的外部对象。





  • 模板的模板参数——两个模板地模板参数指的是同一个模板。





下面是例子:



template<class T, long size> class Array
{ /* ... */ };
void func()
{
Array<char, 2*512> a;
Array<char, 1024> b;
}

编译器预先计算常量表达式象2*512,所以模板ab是相同的类型。下面是另一个例子:



template<class T, int(*error_code_fct)()> class Buffer
{ /* ... */ };
int error_handler();
int another_error_handler();
void func()
{
Buffer<int, &error_handler> b1;
Buffer<int, &another_error_handler> b2;
Buffer<int, &another_error_handler> b3;
Buffer<unsigned int, &another_error_handler> b4;
}

b2b3是同类型的,因为他们是同一模板的实例而且他们接受相同的类型和非类型参数。相反的,b1b4是不同类型。


函数func()从相同类模板实例化出3个不同特殊化:一个b1、第二个b2b3、第三个b4。与普通函数重载不同,编译器对于每一个独有类型从类模板产生一个不同类。在对longint有表示方法的机器上,下面的代码仍然产生两个不同模板的实例:



void too_prolific()
{
Buffer<int, &error_handler> buff1;
Buffer<long, &error_handler> buff2;
}

同样的,charunsigned char实不同类型,即使在默认charunsigned属性的机器上。习惯普通函数重载的宽松匹配规则的程序员并不总能意识到使用有相似但不同类型模板产生的代码膨胀。


避免不必要的实例化


在下面的例子中,产生了模板min的三份拷贝——使用的所有类型:



template < class T > T min(T f, T s)
{
return f < s? f: s;
}
void use_min()
{
int n = 5, m= 10;
int j = min(n,m); //min<int> 实例化
char c = 'a', d = 'b';
char k = min(c,d); //min<char>实例化
short int u = 5, v = 10;
short int w = min(u,v); // min<short int>实例化
}

换句话说,普通函数避免了产生多余的特殊化:



int min(int f, int s)
{
return f < s? f: s;
}
void use_min()
{
//所有调用min都使用了int min (int, int);
int n = 5, m= 10;
int j = min(n,m);
char c = 'a', d = 'b';
char k = min(c,d);
short int u = 5, v = 10;
short int w = min(u,v);
}

尽管如此,min的模板版本比函数有明显的优点:它可以处理指针或其他用户定义类型。你肯定想享用模板好处的同时也避免产生不必要的特殊化的代码。怎么办到呢?简单的解决方案是在调用函数模板之前将参数转换成通用类型。例如:



void no_proliferation()
{
short n = 5, m= 10;
int j = min( static_cast<int> (n),
static_cast<int> (m) ); //min<int> 实例化
char c = 'a', d = 'b';
char k = static_cast<char> (min( static_cast<int> ,
static_cast<int> ) ); //min<int>使用
}

这种技术同样适合指针:首先必须转换成void *,其次void *也可以转换会原来的类型。当呢使用指向多态对象,派生对象的指针转换成基对象的指针。


对于象min这样非常小的模板,将参数转换成公用类型不费多少功夫。尽管如此,使用含上百行代码的大模板时,你就要考虑这个问题。


显式模板实例化


前面提到,如果程序使用了模板,模板仅实例化一次。一般的,当编译器在源文件中碰到模板时,编译器为特殊化产生必要的代码。当编译含有上百源文件的大型工程时,这将严重影响编译器速度,因为碰到特殊化时就必须打断编译过程。这种中断可能在每一个源文件中发生,因为他们都使用——除了意外情况——标准库的类模板。考虑仅包含三个源文件的简单工程:



//filename func1.cpp
#include <string>
#include <iostream>
using namespace std;
void func1()
{
string s; //产生默认构造器和销毁器
s = "hello"; //产生用于const char *的赋值运算符
string s2;
s2 = s; //产生运算符= const string&, string&
cout<<s2.size(); //产生string::size, ostream&运算符 <<(int)
}
//func1.cpp
//filename func2.cpp
#include <string>
#include <iostream>
using namespace std;
void func2()
{
string s; //产生默认构造器和销毁器
cout<<"enter a string: "<<endl; //产生ostream&运算符<<(const char *)
cin>>s //产生istream&运算符>>(string&)
}
//func2.cpp
//filename main.cpp
int main()
{
func1();
func2();
retrun 0;
}
// main.cpp

如果将所有必须的模板代码一次性的实例化,可以大大减少编译时时间,因为避免了打断编译过程。为了这个目的,你可以使用显式实例化(explicit instantiation)。通过在模板申明前面加一个关键字template(没有<>)来指定显式实例化。 这里是一些显式模板实例化的例子:



template <class T> class A{/*..*/};
template<class T> void func(T&) { }
//filename instantiations.cpp
template class Vector<short>; //类模板的显式实例化
template A<int>::A<int>(); //成员函数的显式实例化
template class
std::basic_string<char>; //命名空间成员的显式实例化
template void func<int>(int&); //函数模板的显式实例化

标准库显式实例化的例子

标准库中定义了许多类模板的特殊化。一个例子是类模板basic_string<>。这个类的两个特殊化版本是std::stringstd::wstring,两者分别是特殊化basic_string<char>basic_string<wchar_t>typedef。通常,没有实例化他们的必要,因为头文件<string>已经实例化了特殊化:



#include <string> //td::string和std::wstring的定义
using namespace std;
bool TranslateToKorean(const string& origin,
wstring& target ); //英/韩字典
int main()
{
string EnglishMsg = "This program has performed an illegal operation";
wstring KoreanMsg;
TranslateToKorean(EnglishMsg, KoreanMsg);
}

引出模板





注意:引出模板是C++相对较新的特性;因此并不是所有的编译器都支持它。请检查你的用户手册以确定你的编译器是否支持。



可以在多个translation units中包含同一个模板定义;因此,它可以被编译多次。显然,这浪费了许多编译和连接时间。代替#including完整的模板定义,将模板定义只编译一次而在其他translation units中仅使用模板申明也是可能的。这与编译外部函数和类很相似,其定义只需编译一次之后就只需要申明了。


为了将模板的定义和申明分开,模板必须是exported。通过模板定义前加export来指定:



//filename min.cpp
export template < class T > T min (const T& a, const T& b)
{
return a > b ? b : a;
}

现在要在其他translation units中是用模板只需模板的申明。例如



//file min.c
template < class T > T min (const T & a, const T & b); //仅需申明
int main()
{
int j=0, k=1;
in smaller = min(j,k);
return 0;
}

内联函数模板不能被引出。如果模板函数即申明export又申明了inlineexport申明没有效果,函数仅是内联。申明类模板为引出等价于申明类所有非内联成员函数、静态数据成员和成员类为引出。在未命名命名空间中的模板不能引出。


与语言其他特性的互相影响


模板与语言其他特性的相互影响有时可能带来令人惊讶的结果。下面的章节讨论模板和语言其他特性的各种相互影响,包括模板名字的不明确解释,继承和虚成员函数。


关键字typename


在模板中使用范围名字可能导致类型和非类型的混淆。例如



int N;
template < class T > T func()
{
T::A * N; // 混淆:乘法还是指针申明?
//
}

如果T::A是类型名,func() N的定义创建了指针。另一方面如果T::A是非类型(例如,如果Aint类型的数据类型), T::A * N是一个由范围全名T::A和全局int N相乘组成的表达式。默认的,编译器假定象T::A这样的表达式是非类型。关键字typename指示编译器替代默认的解释,以解决类型名和非类型名的混淆。换句话说,前面(可能混淆的)语句实际上被确定为乘法表达式,结果是被丢弃。为了申明一个指针,需要关键字typename



int N;
template < class T > T func()
{
typename T::A * N; //N现在是一个指针因为T::A是一个类型名
//...
};

模板之间的继承关系


常见的错误是假定派生类的指针或引用的容器是基类的指针或引用的容器。例如



#include<vector>
using namespace std;
class Base
{
public: virtual void f() {}
};
class Derived : public Base
{
public: void f() {}
};
void func( vector<Base*>& vb);
int main()
{
Derived d;
vector<Derived*> vd;
vd.push_back(&d);
func(vd); //错误,vector<Derived*>&不是vector<Base*>
}

尽管DerivedBase之间有继承关系,但是包含有继承关系对象的指针或引用的相同的类模板没有继承种关系。


虚成员函数


成员函数模板不能是虚的。但是类模板的普通成员函数可以是虚函数。例如



template <class T> class A
{
public:
template <class S> virtual void f(S); //error
virtual int g(); // OK
};

成员函数模板的特殊化不会替代在基类中定义的虚函数。例如



class Base
{
public:
virtual void f(char);
};
class Derived : public Base
{
public:
template <class T> void f(T); //不会替代 B::f(int)
};

指向类模板成员函数的指针


指向类成员的指针可以接受类模板成员函数的地址。象普通类一样,成员指针不能指向静态成员函数。爱下面的例子中使用了std::vector<int>的特殊化:



#include<vector>
using namespace std;
//typedef用来隐藏难懂的语法
typedef void (vector< int >::*pmv) (size_t);
void func()
{
pmv reserve_ptr = &vector< int >::reserve;
//...使用reserve_ptr
}

总结


模板简化了实现泛型容器和函数。模板的好处吸引了正在从面向对象的frameworks向面向泛型frameworks转变的软件厂商。然而,参数化类型并不是C++独有的。早在1983,Ada引入了泛型包,一种类似与类模板的东西。其他语言也实现从用户写的框架代码中自动产生代码的机制。


C++中两种模板种类是类模板和函数模板。类模板封装了参数化数据成员和成员函数。函数模板是实现泛型算法的手段。在C++没有模板的时代,传统实现泛型算法的方法是有限、不安全,并不如模板有效率。模板的一个重要方面是支持对象语义。


C++使程序员能通过显式实例化来控制模板的实例化。 实例化整个类。实例化特定成员函数或者部分特殊化函数模板。都是可以的。模板的显式实例化通过不加尖括号的关键字template来指定。类模板的显式实例化忠实需要的。对于函数模板,编译器经常从实参的参数来推出特殊化。定义主模板的参数类模板的特殊化是可以的。这种特性对于修改操作指针类模板的行为是很有用的。部分特殊化通过在模板名字后面的第二参数列表来指定。显式特殊化使得程序员可以代替自动的实例化。显式特殊化通过关键字template指定,其后更着空的尖括号和模板名字后面的参数类型列表。


模板和重载运算符是泛型程序设计的基础构件。标准模板库STL是泛型程序设计的可效仿的framework,就像你将在下一章看到的。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值