有了模板,就可以采用类型安全方法来编写适用于多种类型的单一代码。本章介绍了模板的概念和函数模板上下文中的术语,讨论了更复杂的(更强大的)类模板,描述了模板的组成,此外还讨论了模板实例化、缺省模板参数和模板专门化。本章的结尾部分讨论了模板的潜在问题。
函数模板描述了仅用参数或返回值的类型来区分的一组相关函数。
使用模板之前,请先声明。以下示例中的声明提供了使用模板所需的足够信息,但没有提供实现模板所需的足够信息。
template <class Number> Number twice( Number original ); |
在此示例中,Number 是模板参数,它指定模板描述的函数范围。更具体地说,Number 是模板类型参数,在模板定义中使用它表示确定的模板使用位置处的类型。
如果要声明模板,请先定义该模板。定义提供了实现模板所需的足够信息。以下示例定义了在前一个示例中声明的模板。
template <class Number> Number twice( Number original ) { return original + original; } |
因为模板定义通常出现在头文件中,所以模板定义必须在多个编译单元中重复。不过所有的定义都必须是相同的。该限制称为一次定义规则。
声明后,模板可以像其他函数一样使用。它们的使用由命名模板和提供函数参数组成。编译器可以从函数参数类型推断出模板类型参数。例如,您可以使用上面声明的模板,具体步骤如下所示。
double twicedouble( double item ) { return twice( item ); } |
如果模板参数不能从函数参数类型推断出,则调用函数时必须提供模板参数。例如:
template<class T> T func(); // no function arguments int k = func<int>(); // template argument supplied explicitly |
类模板描述了一组相关的类或数据类型,它们只能通过类型来区分:整数值、指向(或引用)具有全局链接的变量的指针、其他的组合。类模板尤其适用于描述通用但类型安全的数据结构。
类模板声明仅提供了类的名称和类的模板参数。此类声明是不完整的类模板。
以下示例是名为 Array 类的模板声明,该类可接受任何类型作为参数。
template <class Elem> class Array; |
该模板用于名为 String 的类,该类接受 unsigned int 作为参数。
template <unsigned Size> class String; |
类模板定义必须声明类数据和函数成员,如以下示例所示。
template <class Elem> class Array { Elem* data; int size; public: Array( int sz ); int GetSize(); Elem& operator[]( int idx ); }; |
template <unsigned Size> class String { char data[Size]; static int overflows; public: String( char *initial ); int length(); }; |
与函数模板不同,类模板可以同时有类型参数(如 class Elem)和表达式参数(如 unsigned Size)。表达式参数可以是:
具有整型或枚举的值
指向对象的指针或到对象的引用
指向函数的指针或到函数的引用
指向类成员函数的指针
类模板的完整定义需要类模板函数成员和静态数据成员的定义。动态(非静态)数据成员由类模板声明完全定义。
模板函数成员的定义由模板参数专门化后跟函数定义组成。函数标识符通过类模板的类名称和模板参数限定。以下示例说明了 Array 类模板的两个函数成员的定义,该模板中指定了模板参数 template <class Elem>。每个函数标识符都通过模板类名称和模板参数 Array<Elem> 限定。
template <class Elem> Array<Elem>::Array( int sz ) {size = sz; data = new Elem[size];} template <class Elem> int Array<Elem>::GetSize() { return size; } |
该示例说明了 String 类模板的函数成员的定义。
#include <string.h> template <unsigned Size> int String<Size>::length( ) {int len = 0; while (len < Size && data[len]!= ’\0’) len++; return len;} template <unsigned Size> String<Size>::String(char *initial) {strncpy(data, initial, Size); if (length( ) == Size) overflows++;} |
模板静态数据成员的定义由后跟变量定义的模板参数专门化组成,在此处变量标识符通过类模板名称和类模板实元参数来限定。
template <unsigned Size> int String<Size>::overflows = 0; |
模板类可以在使用类型的任何地方使用。指定模板类包括了提供模板名称和参数的值。以下示例中的声明根据 Array 模板创建 int_array 变量。变量的类声明及其一组方法类似于 Array 模板中的声明和方法,除了 Elem 替换为 int(请参6.3 模板实例化)。
Array<int> int_array(100); |
此示例中的声明使用 String 模板创建 short_string 变量。
String<8> short_string("hello"); |
需要任何其他成员函数时,您可以使用模板类成员函数。
int x = int_array.GetSize( ); |
int x = short_string.length( ); . |
模板实例化是生成采用特定模板参数组合的具体类或函数(实例)。例如,编译器生成一个采用 Array<int> 的类,另外生成一个采用 Array<double> 的类。通过用模板参数替换模板类定义中的模板参数,可以定义这些新的类。在前面“类模板”一节介绍的 Array<int> 示例中,编译器用 int 替换所有 Elem。
使用模板函数或模板类时需要实例。如果这种实例还不存在,则编译器隐式实例化模板参数组合的模板。
编译器仅为实际使用的那些模板参数组合而隐式实例化模板。该方法不适用于构造提供模板的库。C++ 提供了显式实例化模板的功能,如以下示例所示。
要显式实例化模板函数,请在 template 关键字后接函数的声明(不是定义),且函数标识符后接模板参数。
template float twice<float>(float original); |
在编译器可以推断出模板参数时,模板参数可以省略。
template int twice(int original); |
要显式实例化模板类,请在 template 关键字后接类的声明(不是定义),且在类标识符后接模板参数。
template class Array<char>; |
template class String<19>; |
显式实例化类时,所有的类成员也必须实例化。
要显式实例化模板类函数成员,请在 template 关键字后接函数的声明(不是定义),且在由模板类限定的函数标识符后接模板参数。
template int Array<char>::GetSize(); |
template int String<19>::length(); |
要显式实例化模板类静态数据成员,请在 template 关键字后接成员的声明(不是定义),且在由模板类限定的成员标识符后接模板参数。
template int String<19>::overflows; |
可以嵌套使用模板。这种方式尤其适用于在通用数据结构上定义通用函数,与在标准 C++ 库中相同。例如,模板排序函数可以通过一个模板数组类进行声明:
template <class Elem> void sort(Array<Elem>); |
并定义为:
template <class Elem> void sort(Array<Elem> store) {int num_elems = store.GetSize(); for (int i = 0; i < num_elems-1; i++) for (int j = i+1; j < num_elems; j++) if (store[j-1] > store[j]) {Elem temp = store[j]; store[j] = store[j-1]; store[j-1] = temp;}} |
上述示例定义了针对预先声明的 Array 类模板对象的排序函数。下一个示例说明了排序函数的实际用法。
Array<int> int_array(100); // construct an array of ints sort(int_array); // sort it |
您可以将缺省值赋予类模板(但不是函数模板)的模板参数。
template <class Elem = int> class Array; template <unsigned Size = 100> class String; |
如果模板参数具有缺省值,则该参数后的所有参数也必须具有缺省值。模板参数仅能具有一个缺省值。
将某些模板参数组合视为特殊的参数可以提高性能,如以下 twice 示例所示。而模板说明可能无法用于其一组可能的参数,如以下 sort 示例所示。模板专门化允许您定义实际模板参数给定组合的可选实现。模板专门化覆盖了缺省实例化。
使用模板参数的组合之前,您必须声明专门化。以下示例声明了twice 和 sort 的专用实现。
template <> unsigned twice<unsigned>( unsigned original ); |
template <> sort<char*>(Array<char*> store); |
如果编译器可以明确决定模板参数,则您可以省略模板参数。例如:
template <> unsigned twice(unsigned original); |
template <> sort(Array<char*> store); |
必须定义声明的所有模板专门化。下例定义了上一节中声明的函数。
template <> unsigned twice<unsigned>(unsigned original) {return original << 1;} |
#include <string.h> template <> void sort<char*>(Array<char*> store) {int num_elems = store.GetSize(); for (int i = 0; i < num_elems-1; i++) for (int j = i+1; j < num_elems; j++) if (strcmp(store[j-1], store[j]) > 0) {char *temp = store[j]; store[j] = store[j-1]; store[j-1] = temp;}} |
专门化与其他任何模板一样使用并实例化,除此以外,完全专用模板的定义也是实例化。
在前一个示例中,模板是完全专用的。也就是说,模板定义了特定模板参数的实现。模板也可以部分专用,这意味着只有某些模板参数被指定,或者一个或多个参数被限定到某种类型。生成的部分专门化仍然是模板。例如,以下代码样本说明了主模板和该模板的完全专门化。
template<class T, class U> class A {...}; //primary template template<> class A<int, double> {...}; //specialization |
以下代码说明了主模板部分专门化的示例。
template<class U> class A<int> {...}; // Example 1 template<class T, class U> class A<T*> {...}; // Example 2 template<class T> class A<T**, char> {...}; // Example 3 |
示例 1 提供了用于第一个模板参数是 int 类型的情况的特殊模板定义。
示例 2 提供了用于第一个模板参数是任何指针类型的情况的特殊模板定义。
示例 3 提供了用于第一个模板参数是任何类型的指针到指针而第二个模板参数是 char 类型的情况的特殊模板定义。
本节描述了使用模板时会遇到的问题。
有时模板定义使用模板参数或模板本身未定义的名称。如此,编译器解决了封闭模板作用域的名称,该模板可以在定义或实例化点的上下文中。名称可以在不同的位置具有不同的含义,产生不同的解析。
名称解析比较复杂。因此,您不应该依赖除一般全局环境中提供的名称外的非本地名称。也就是说,仅使用在任何地方都用相同方法声明和定义的非本地名称。在以下示例中,模板函数 converter 使用了非本地名称 intermediary 和 temporary。在 use1.cc 和 use2.cc 中这些名称的定义不同,因此在不同的编译器下可能会生成不同的结果。为了能可靠地使用模板,所有非本地名称(该示例中为 intermediary 和 temporary)在任何地方都必须有相同的定义。
use_common.h // Common template definition template <class Source, class Target> Target converter(Source source) {temporary = (intermediary)source; return (Target)temporary;} use1.cc typedef int intermediary; int temporary; #include "use_common.h" use2.cc typedef double intermediary; unsigned int temporary; #include "use_common.h" |
一个常见的非本地名称用法是在模板内使用 cin 和 cout 流。有时程序员要将流作为模板参数传递,这时就要引用到全局变量。但 cin 和 cout 在任何地方都必须有相同的定义。
模板实例化系统取决于类型名称,等效于决定哪些模板需要实例化或重新实例化。因此本地类型用作模板参数时,会导致严重的问题。小心在代码中也出现类似的问题。例如:
array.h template <class Type> class Array { Type* data; int size; public: Array(int sz); int GetSize(); }; array.cc template <class Type> Array<Type>::Array(int sz) {size = sz; data = new Type[size];} template <class Type> int Array<Type>::GetSize() {return size;} file1.cc #include "array.h" struct Foo {int data;}; Array<Foo> File1Data(10); file2.cc #include "array.h" struct Foo {double data;}; Array<Foo> File2Data(20); |
在 file1.cc 中记录的 Foo 类型与在 file2.cc 中记录的 Foo 类型不同。以这种方法使用本地类型会出现错误和意外的结果。
模板在使用之前必须先声明。模板的使用由友元声明构成,不是由模板的声明构成。实际的模板声明必须在友元声明之前。例如,编译系统尝试链接以下示例中生成的目标文件时,对未实例化的 operator<< 函数,会生成未定义错误。
array.h // generates undefined error for the operator<< function #ifndef ARRAY_H #define ARRAY_H #include <iosfwd> template<class T> class array { int size; public: array(); friend std::ostream& operator<<(std::ostream&, const array<T>&); }; #endif array.cc #include <stdlib.h> #include <iostream> template<class T> array<T>::array() {size = 1024;} template<class T> std::ostream& operator<<(std::ostream& out, const array<T>& rhs) {return out <<’[’ << rhs.size <<’]’;} main.cc #include <iostream> #include "array.h" int main() { std::cout << "creating an array of int... " << std::flush; array<int> foo; std::cout << "done\n"; std::cout << foo << std::endl; return 0; } |
请注意,因为编译器将以下代码作为普通函数(array 类的 friend)的声明进行读取,所以编译期间不会出现错误消息。
friend ostream& operator<<(ostream&, const array<T>&); |
因为 operator<< 实际上是模板函数,所以需要在声明 template class array 之前提供模板声明。但是,由于 operator<< 有一个 array<T> 类型的参数,因此必须在声明函数之前声明 array<T>。文件 array.h 必须如下所示:
#ifndef ARRAY_H #define ARRAY_H #include <iosfwd> // the next two lines declare operator<< as a template function template<class T> class array; template<class T> std::ostream& operator<<(std::ostream&, const array<T>&); template<class T> class array { int size; public: array(); friend std::ostream& operator<< <T> (std::ostream&, const array<T>&); }; #endif |
C++ 标准要求使用具有限定名的类型,这些限定名取决于要用 typename 关键字显式标注为类型名称的模板参数。即使编译器“知道”它应该是一个类型,也是如此。以下示例中的注释说明了具有要用 typename 关键字的限定名的类型。
struct simple { typedef int a_type; static int a_datum; }; int simple::a_datum = 0; // not a type template <class T> struct parametric { typedef T a_type; static T a_datum; }; template <class T> T parametric<T>::a_datum = 0; // not a type template <class T> struct example { static typename T::a_type variable1; // dependent static typename parametric<T>::a_type variable2; // dependent static simple::a_type variable3; // not dependent }; template <class T> typename T::a_type // dependent example<T>::variable1 = 0; // not a type template <class T> typename parametric<T>::a_type // dependent example<T>::variable2 = 0; // not a type template <class T> simple::a_type // not dependent example<T>::variable3 = 0; // not a type |
由于 ">>" 字符序列解释为右移运算符,因此在一个模板名称中使用另一个模板名称时必须小心。确保相邻的 ">" 字符之间至少有一个空格。
例如,以下是形式错误的语句:
Array<String<10>> short_string_array(100); // >> = right-shift |
被解释为:
Array<String<10 >> short_string_array(100); |
正确的语法为:
Array<String<10> > short_string_array(100); |
在模板定义中,编译器不支持引用在全局作用域或名称空间中声明为静态的对象或函数。如果生成了多个实例,则每个示例引用到了不同的对象,因此违背了单次定义规则(C++ 标准的 3.2 节)。通常的失败指示是链接时丢失符号。
如果想要所有模板实例化共享单一对象,那么请使对象成为已命名空间的非静态成员。如果想要模板类的每个实例化不同对象,那么请使对象成为模板类的静态成员。如果希望每个模板函数实例化的对象不同,请使对象成为函数的本地对象。
如果要通过指定 -instances=extern 生成多个程序或库,建议在不同的目录中生成这些程序或库。如果要在同一目录中生成多个程序,那么您需要清除不同生成程序之间的系统信息库。这样可以避免出现任何不可预料的错误。有关更多信息,请参见7.4.4 共享模板系统信息库。
考虑以下示例的 make 文件 a.cc、b.cc、x.h 和 x.cc。请注意,仅当指定了 -in tances=extern 时,此示例才有意义:
........ Makefile ........ CCC = CC all: a b a: $(CCC) -I. -instances=extern -c a.cc $(CCC) -instances=extern -o a a.o b: $(CCC) -I. -instances=extern -c b.cc $(CCC) -instances=extern -o b b.o clean: /bin/rm -rf SunWS_cache *.o a b |
... x.h ... template <class T> class X { public: int open(); int create(); static int variable; }; |
... x.cc ... template <class T> int X<T>::create() { return variable; } template <class T> int X<T>::open() { return variable; } template <class T> int X<T>::variable = 1; |
... a.cc ... #include "x.h" int main() { X<int> temp1; temp1.open(); temp1.create(); } |
... b.cc ... #include "x.h" int main() { X<int> temp1; temp1.create(); } |
如果同时生成了 a 和 b,请在两个生成之间添加 make clean。以下命令会引起错误:
example% make a example% make b |
以下命令不会产生任何错误:
example% make a example% make clean example% make b |