一、模板定义:
1. 模板形参表不能为空。
2. 模板形参可以是表示类型的类型形参,也可以是表示常量表达式的非类型形参。非类型形参跟在类型说明符之后声明。
3. 使用函数模板时,可以由编译器去推导出实际模板实参;但是使用类模板时,必须为模板形参显式指定实参。
4. 模板形参遵循常规名字屏蔽规则。与全局作用域中声明的对象、函数或类型同名的模板形参会屏蔽全局名字。
5. 模板类型形参可以用于指定返回类型或函数形参类型,以及在函数体中用于变量声明或强制类型转换。
6. 在函数模板形参表中,关键字typename和class具有相同的含义,可以互换使用,但是,关键字typename是作为C++的组成部分加入到C++中,因此旧的程序更有可能只用关键字class。
7. 如果要在函数模板内部使用类中定义的类型成员,必须告诉编译器我们正在使用的名字指的是一个类型。默认情况下,编译器假定这样的名字指定数据成员,而不是类型成员。
1 template <class Parm, class U> 2 Parm fun(Parm * array, U value) 3 { 4 typename Parm::size_type * p; // typename不可缺少 5 // 否则size_type被编译器认为 6 // 是一个数据成员的名字。 7 }
8. 模板费类型形参是模板定义内部的常量值,在需要常量表达式的时候,可使用非类型形参指定数组长度:
1 template <class T, size_t N> 2 void array_init(T (& parm)[N]) 3 { 4 }
参数是一个N个T类型元素数组的引用,函数形参中数组长度必须固定,通过模板非类型形参N,使函数形参数组长度可变。
1 int x[42]; 2 double y[10]; 3 array_init(x); // array_init(int (& parm)[42]) 4 array_init(y); // array_init(double (& parm)[10])
9. 对模板的非类型形参,求值结果相同的表达式将认为是等价的。
10. 编写泛型代码的重要原则:
模板的形参是const引用;
函数体中的测试只用<比较;
二、实例化:
1. 类模板在引用实际模板类类型时实例化,函数模板在调用它或用它对函数指针进行初始化或赋值时实例化。
2. 类模板形参是必须的,想要使用类模板,必须显式指定模板实参。
3. 使用函数模板时,编译器通常会为我们推断模板实参。
4. 模板实参推断:
(1)多个类型形参的实参必须完全匹配;如果推断的类型不能完全匹配,则调用会出错,模板实参推断失败。
1 template <typename T> 2 int compare(const T & v1, const T & v2) {} 3 compare(3.14, 3); // 无法完全匹配,模板实参推断失败
(2)一般而言,不会转换实参以匹配已有的实例化。接收const引用或const指针的函数可以分别用非const对象的引用或指针来调用,无须产生洗的实例化。无论传递const或非const对象给接收非引用类型的函数,都使用相同的实例化。如果模板形参不是引用类型,则数组实参将当做指向其第一个元素的指针,函数实参当做指向函数类型的指针。
1 template <typename T> 2 T fobj(T, T); 3 template <typename T> 4 T fref(const T &, const T &); 5 int a[10], b[42]; 6 fobj(a, b); // fobj(int *, int *);数组大小不同无关紧要 7 fref(a, b); // error,模板形参为引用类型,无法转换
(3)类型转换的限制只适用于类型为模板形参的那些实参,用普通类型定义的形参可以使用常规转换。
1 template <typename T> 2 T sum(const T & op1, int op2);
(4)可以使用函数模板对函数指针进行初始化或赋值,编译器使用指针的类型实例化具有适当模板实参的模板版本。
1 template <typename T> 2 int compare(const T &, const T &); 3 int (* pf1)(const int &, const int &) = compare;
如果不能从函数指针类型确定模板实参,将产生编译时或链接时错误:
1 void func(int (*)(const string &, const string &)); 2 void func(int (*)(const int &, const int &)); 3 func(compare); // error,无法确定是哪一个实例化
5. 某些情况下,不可能推断模板实参的类型,当函数的返回类型必须与形参表中所用的所有类型都不同时,最常出现这一问题。
1 template <typename T, typename U> 2 ??? sum(T, U);
希望返回类型足够大,可以包含任意次序传递的任意两个类型的两个值的和。
解决方案是:强制sum的调用者将较小的类型强制转换为希望作为结果使用的类型。
1 int i; short s; 2 sum(static_cast<int>(s), i);
6. 指定返回类型的一种方式是引入第三个模板参数:
1 template <class T1, class T2, class T3> 2 T1 sum(T2, T3);
对于T1,由于没有实参的类型可用于推断T1的类型,因此,必须每次调用sum时为该形参显式提供实参:
1 long val = sum<long>(i, lng); // ok: long sum(int, long);
显式模板实参从左到右进行匹配,只有“最右边”的形参的显式模板实参可以忽略:
1 template <class T1, class T2, class T3> 2 T2 sum(T1, T3); 3 // calls long sum(int, long); 4 long val = sum<int, long, long>(i, lng);
7. 显式实参与函数模板指针:
1 void func(int (*)(const string &, const string &)); 2 void func(int (*)(const int &, const int &)); 3 func(compare<int>); // ok
三、模板编译模型:
1. 一般而言,当调用函数的时候,编译器只需要看到函数的声明。定义类类型的对象时,类定义必须可用,但成员函数的定义不是必须存在的。因此,应该将类定义和函数声明放在头文件中,而普通函数和类成员函数的定义放在源文件中。
2. 模板需要进行实例化,编译器必须能够访问定义模板的源代码。当调用函数模板或类模板的成员函数时,编译器需要函数定义,需要那些通常放在原文件中的代码。
3. 包含编译模型:
保持头文件和实现文件的分离,在.h文件中进行#include ".cpp"包含,举例:
1 // A.h 2 template <typename T> 3 int compare(const T &, const T &); 4 #include "A.cpp" 5 6 // A.cpp 7 template <typename T> 8 int compare(const T &, const T &) 9 { 10 }
注意:旧式编译器对每个文件中的模板实例化超过一次,产生多个实例,编译器选择一个实例而抛弃其他的,导致编译时性能显著降低。
4. 分别编译模型:
export关键字能够指明给定的定义可能会需要在其他文件中产生实例化。在一个程序中,一个模板只能定义为导出一次,export关键字不必在模板声明中出现。对类模板声明export更复杂一些。通常,类声明必须放在头文件中,头文件中的类定义体不应该使用关键字export,如果在头文件中使用了export,则该头文件只能被程序中的一个源文件使用。相反,应该在类的实现文件中使用export:
1 // A.h 2 template <typename T> 3 class Queue {}; 4 5 // A.cpp 6 export template <typename T> class Queue; 7 #include "A.h" 8 // Queue 成员定义
注意:上述.cpp代码中导出类的成员将自动声明为导出的。也可以将类模板中的个别成员声明为导出的,此时,关键字export不再类模板本身指定,而是只在被导出的特定成员定义上指定。导出成员函数的定义不必再使用成员时可见。任意非导出成员的定义必须像在包含模型中一样对待:定义应放在定义类模板的头文件中。
四、类模板成员:
1. 当使用类模板的名字的时候,必须指定模板形参,这一规则有个例外:在类本身的作用域内部,可以使用类模板的非限定名。例如,在Queue类的默认构造函数苏、赋值构造函数、赋值操作符、析构函数中,名字Queue是Queue<T>的缩写表示。实质上,编译器推断,当引用类的名字时,引用的是同一版本:
1 Queue(const Queue & Q);
等价于:
1 Queue<T>(const Queue<T> & Q);
但是,编译器不会对类中使用的其他模板的模板形参进行这样的推断。
2. 类模板的成员函数本身也是函数模板。像任何其他函数模板一样,需要使用类模板的成员函数产生该成员的实例化。与其他函数模板不同的是,在实例化类模板成员函数的时候,编译器不执行模板实参推断,相反,类模板成员函数的模板形参由调用该函数的对象的类型确定,例如,调用Queue<int>类型对象的push成员函数时,实例化的push函数为:
1 void Queue<int>::push(const int & val);
对象的模板实参能够确定成员函数模板形参。这一事实意味着,调用类模板成员函数比调用类似函数模板更灵活。用模板形参定义的函数形参的实参允许进行常规转换。
3. 类模板的成员函数只有为程序所用才进行实例化。如果某函数从未使用,则不会实例化该成员函数。这一行为意味着,用于实例化模板的类型只需要满足实际使用的操作的要求。
4. 定义模板类型的对象时,该定义导致实例化类模板,定义对象也会实例化用于初始化该对象的任一构造函数,以及该构造函数调用的任意成员。
5. 非类型形参的模板实参必须是编译时常量表达式。
6. 类模板中的友元声明:
(1)普通非模板类或非模板函数友元:
1 template <class T> 2 class Bar 3 { 4 friend class FooBar; 5 friend void fun(); 6 };
语义:FooBar的成员和fun函数可以访问Bar类的任意实例的private成员和protected成员。
(2)类模板或函数模板友元:
1 template <class Type> 2 class Bar 3 { 4 template <class T> friend class Foo; 5 template <class T> friend void fun(const T &); 6 };
语义:这些友元声明使用与类本身不同的类型参数,Foo的任意实例都可以访问Bar的任意实例的私有元素,类似的,fun的任意实例可以访问Bar的任意实例。
(3)只授予对类模板或函数模板的特定实例的访问权的友元声明:
1 template <class T> class Foo; 2 template <class T> void fun(const T &); 3 template <class T> 4 class Bar 5 { 6 friend class Foo<char *>; 7 friend void fun<char *>(char * const &); 8 };
语义:形参类型为char *的Foo和fun的特定实例可以访问Bar的每个实例。
7. 实质上,编译器将友元声明也当做类或函数的声明对待,但是,要想限制对特定实例化的友元关系时,必须在可以用于友元声明之前声明类或函数:
1 template <class T> class A; 2 template <class T> 3 class B 4 { 5 friend class A<T>; // ok,且建立一对一映射 6 friend class E<T>; // error 7 friend class F<int>; // error 8 };
如果没有事先告诉编译器该友元是一个模板,则编译器将认为该友元是一个普通非模板类或模板函数。
8. 成员模板:
1 template <class Type> 2 class Queue 3 { 4 template <class T> void fun(const T &); // 成员模板 5 };
在类外部定义成员模板:
1 template <class Type> template <class T> 2 void Queue<Type>::fun(const T & val) 3 { 4 }
成员模板遵循常规的public、protected、private访问控制。
与其他成员一样,成员模板只有在程序中使用时才实例化。成员模板有两种模板形参:由类定义的和有成员模板定义的。类模板形参由调用函数的对象的类型确定,成员定义的模板形参的行为与普通函数模板一样,这些形参都通过常规模板实参推断而确定。
9. 类模板可以像其他任意类一样声明static成员。
1 template <class Type> 2 class Foo 3 { 4 public: 5 static std::size_t ctr; 6 };
每个实例化表示截然不同的类型,所以给定实例化的所有对象都共享一个static成员。因此,Foo<int>类型的任意对象共享同一个static成员ctr;Foo<string>类型的对象共享另一个不同的ctr成员。
10. 静态成员的访问:
1 Foo<int> f; 2 size_t tmp = f.ctr; 3 size_t st = Foo<int>::ctr;
定义方式像其他任意static数据成员一样,必须在类外部出现数据成员的定义,例如:
1 template <class Type> 2 size_t Foo<Type>::ctr = 0;
五、一个泛型句柄类:
1. Handle类的行为类似于指针:复制Handle对象将不会复制基础对象(即Handle内部的成员指针指向对象),复制之后,两个Handle对象将引用同一基础对象。要创建Handle对象,用户需要传递属于由Handle管理的类型(或从该类型派生的类型)的动态分配对象的地址,从此刻起,Handle将“拥有”这个对象。而且,一旦不再有任意Handle对象与该对象关联,Handle类将负责删除该对象。
2. 泛型Handle类的实现 && 使用句柄类:
1 // Handle.h 2 #include <iostream> 3 4 template <class T> 5 class Handle 6 { 7 public: 8 Handle(T * p = NULL); 9 Handle(const Handle & h); 10 Handle & operator=(const Handle & rhs); 11 ~Handle(); 12 T & operator*(); 13 T * operator->(); 14 const T & operator*() const; 15 const T * operator->() const; 16 protected: 17 private: 18 T * ptr; 19 size_t * use; 20 void rem_ref(); 21 }; 22 23 template <class T> 24 Handle<T>::Handle(T * p /* = NULL */) : ptr(p), use(new size_t(1)) 25 { 26 27 } 28 29 template <class T> 30 Handle<T>::Handle(const Handle & h) : ptr(h.ptr), use(h.use) 31 { 32 ++*use; 33 } 34 35 template <class T> 36 Handle<T>::~Handle() 37 { 38 rem_ref(); 39 } 40 41 template <class T> 42 void Handle<T>::rem_ref() 43 { 44 if (--*use == 0) 45 { 46 delete ptr; 47 delete use; 48 } 49 } 50 51 template <class T> 52 Handle<T> & Handle<T>::operator=(const Handle & rhs) 53 { 54 ++*rhs.use; 55 rem_ref(); 56 ptr = rhs.ptr; 57 use = rhs.use; 58 59 return *this; 60 } 61 62 template <class T> 63 T & Handle<T>::operator*() 64 { 65 if (ptr) 66 { 67 return *ptr; 68 } 69 else 70 { 71 throw std::runtime_error("dereference of unbound Handle"); 72 } 73 } 74 75 template <class T> 76 T * Handle<T>::operator->() 77 { 78 if (ptr) 79 { 80 return ptr; 81 } 82 else 83 { 84 throw std::runtime_error("access through unbound Handle"); 85 } 86 } 87 88 template <class T> 89 const T & Handle<T>::operator *() const 90 { 91 if (ptr) 92 { 93 return *ptr; 94 } 95 else 96 { 97 throw std::runtime_error("dereference of unbound Handle"); 98 } 99 } 100 101 template <class T> 102 const T * Handle<T>::operator->() const 103 { 104 if (ptr) 105 { 106 return ptr; 107 } 108 else 109 { 110 throw std::runtime_error("access through unbound Handle"); 111 } 112 } 113 114 // test.cpp 115 #include "Handle.h" 116 117 int main() 118 { 119 Handle<int> hp(new int(42)); 120 if (true) 121 { 122 Handle<int> hp2 = hp; 123 std::cout << *hp << " " << *hp2 << std::endl; 124 *hp2 = 10; 125 } 126 std::cout << *hp << std::endl; 127 128 return 0; 129 }
六、模板特化:
1. 模板特化解决的问题是:某些情况下,通用模板定义对于某个类型可能是完全错误的,如下模板对于char *是完全没有意义的:
1 template <class T> 2 int compare(const T & v1, const T & v2) 3 { 4 if (v1 < v2) return -1; 5 if (v2 < v1) return 1; 6 return 0; 7 }
2. 定义当模板形参类型绑定到const char *时,compare函数的特化:
1 template <> 2 int compare<const char *>(const char * const v1, 3 const char * const v2) 4 { 5 return strcmp(v1, v2); 6 }
3. 模板实例化需要注意的一点:
1 template <class T> 2 void fun(const T r) 3 { 4 }
则:fun<char *>()参数中的r将是char * const类型。
fun<const char *>()参数中的r将是const char * const类型。
1 template <class T> 2 void fun(const T & r) 3 { 4 }
则:fun<char *>()参数中的r将是char *的const引用。
fun<const char *>()参数中的r将是const char *的const引用。
4. 根据2中的模板特化,当调用compare函数时,传递给它两个常量字符指针const char *,编译器将调用特化版本。编译器将为任意其他实参类型(包括普通char *)调用泛型版本:
1 const char * cp1 = "world", * cp2 = "hi"; 2 int i1, i2; 3 compare(cp1, cp2); // 调用特化版本 4 compare(i1, i2); // 调用泛型版本
5. 声明模板特化:
1 template <> 2 int compare<const char *>(const char * const &, 3 const char * const &);
或者如果可从函数形参表推断模板实参,则不必显式指定模板实参:
1 template <> 2 int compare(const char * const &, 3 const char * const &);
6. 在特化中省略空的模板形参表template <>将导致变成重载非模板版本:
1 int compare(const char * const &, const char * const &);
以上声明了一个普通函数,该函数含有返回类型和可与模板实例化匹配的形参表。
7. 当定义非模板函数的时候,对实参应用常规转换;当特化模板的时候,对实参类型不应用转换。
8. 不是总能检测到重复定义:
如果程序由多个文件构成,模板特化的声明必须在使用该特化的每个文件中出现。不能在一些文件中从泛型模板定义实例化一个函数模板,而在其他文件中为同一模板实参集合特化该函数。
9. 与其他函数声明一样,应在一个头文件中包含模板特化的声明,然后使用该特化的每个源文件中包含该头文件。
10. 普通作用域规则适用于特化:
1 template <class T> 2 int compare(const T & t1, const T & t2) {} 3 4 int main() 5 { 6 int i = compare("hello", "world"); 7 } 8 9 template <> 10 int compare<const char *>(const char * const & s1, 11 const char * const & s2) 12 { 13 }
特化出现在对该模板实例的调用之后是错误的。对具有同一模板实参集的同一模板,程序不能既有显式特化又有实例化。在声明特化之前,进行了可以与特化相匹配的一个调用,当编译器看到一个函数调用时,它必须知道这个版本需要特化,否则,编译器将可能从模板定义实例化该函数。
11. 类模板特化:
特化可以定义与模板本身完全不同的成员。如果一个特化无法从模板定义某个成员,该特化类型的对象就不能使用该成员。类模板成员的定义不会用于创建显式特化成员的定义。类模板特化应该与它所特化的模板定义相同的接口,否则当用户试图使用未定义的成员时会感到奇怪。
12. 在类特化外部定义成员时,成员之前不能加template <>标记:
1 void Queue<const char *>::push(const char * val) {}
13. 类模板的部分特化:
如果类模板有一个以上的模板形参,也许想要特化某些模板形参而非全部,可以使用类模板的部分特化:
1 template <class T1, class T2> 2 class some_template 3 { 4 } 5 // 部分特化: 6 template <class T1> 7 class some_template<T1, int> 8 { 9 }
类模板的部分特化本身也是模板。
14. 使用类模板的部分特化:
1 some_template <int, string> foo; // 使用模板 2 some_template <string, int> bar; // 使用部分特化
部分特化的定义与通用模板的定义完全不会冲突。部分特化可以具有与通用类模板完全不同的成员集合。类模板成员的通用定义永远不会用来实例化类模板部分特化的成员。
七、重载与函数模板:
1. 函数模板可以重载:可以定义有相同名字但形参数目或类型不同的多个函数模板,也可以定义与函数模板有相同名字的普通非模板函数。
2. 声明一组重载函数模板不保障可以成功调用它们,重载的函数模板可能会导致二义性。
3. 设计既包含函数模板又包含非模板函数的重载函数集合是困难的,因为可能会使函数的用户感到奇怪,定义函数模板特化几乎总是比使用非模板版本更好。
浙公网安备 33010602011771号