博客园  :: 首页  :: 新随笔  :: 联系 :: 订阅 订阅  :: 管理

C++ - 模板与泛型编程

Posted on 2012-10-01 14:11  神の大司命  阅读(501)  评论(0)    收藏  举报

一、模板定义:

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. 设计既包含函数模板又包含非模板函数的重载函数集合是困难的,因为可能会使函数的用户感到奇怪,定义函数模板特化几乎总是比使用非模板版本更好。