第四部分 面向对象编程与泛型编程
第十六章 模板与泛型编程
所谓泛型编程是以独立于任何特定类型的方式编写代码,使用泛型程序时,我们需要提供具体程序实例所操作的类型或值,模板是泛型编程的基础,使用模板时无须了解模板的定义,在泛型编程中,我们所编写的类或函数能够多态地用于跨越编译时不相关的类型,一个类或一个函数可以用来操纵多种类型的对象
16.1 定义模板
16.1.1 定义函数模板
模板定义,以关键字 template 开始,后接 模板形参表(template parameter list,不能为空),模板形参表是用尖括号括住的一个或多个模板形参(template parameter)的列表,模板形参 可以是 表示类型的类型形参(type parameter),也可以 表示常量表达式的非类型形参(nontype parameter)
#include <string> using std::string; #include <iostream> using std::cout; using std::endl; #include <cstddef> using std::size_t; // when we call print the compiler will instantiate a version of print // with T replaced by the argument's element type, and N by that array's dimension template <typename T, size_t N> void print(T (&arr)[N]) { for (auto elem : arr) cout << elem << endl; } int main() { int a1[] = {0,1,2,3,4,5,6,7,8,9}; int a2[] = {1,3,5}; string a3[4]; print(a1); // instantiates print(int (&arr)[10]) print(a2); // instantiates print(int (&arr)[3]) print(a3); // instantiates print(string (&arr)[42]) return 0; }
16.1.2 定义类模板
类模板也是模板,必须以关键字template开头,后接模板形参表,使用类模板,必须为模板形参显式指定实参,编译器使用实参来实例化这个类的特定类型版本
16.1.3 模板形参
模板形参的名字可以在声明为模板形参之后直到模板声明或定义的末尾处使用,模板形参遵循常规名字屏蔽规则,用作模板形参的名字不能在模板内部重用,同一模板的声明或定义中,模板形参的名字不必相同
16.1.4 模板类型形参
类型形参由关键字class或typename后接说明符构成,它可以用于指定返回类型或函数形参类型,以及在函数体中用于变量声明或强制类型转换,处理定义数据成员或函数成员之外,类还可以定义类型成员,通过在成员名前加上关键字typename作为前缀,可以告诉编译器将成员当作类型
16.1.5 非类型模板形参
模板形参不必都是类型,在调用函数时,非类型形参将用值代替,值的类型在模板形参表中指定,模板非类型形参是模板定义内部的常量值,在需要常量表达式的时候,可使用非类型形参
16.1.6 编写泛型程序
编写模板时,代码不可能针对特定类型,但模板代码总是要对将使用的类型做一些假设,产生的程序是否合法,取决于函数中使用的操作以及所用类型支持的操作,在函数模板内部完成的操作 限制了可用于实例化该函数的类型,编写模板代码时,对实参类型的要求尽可能少是很有益的
编写泛型代码的两个重要原则:模板的形参是const引用 (可用于不允许复制的类型,且引用较大的对象,函数运行更快);函数体中的测试只用<比较 (减少对实参类型的要求)
16.2 实例化
模板是一个蓝图,它本身不是类或函数,编译器用模板产生指定的类或函数的特定类型版本,产生模板的特定类型实例的过程称为实例化:模板在使用时将进行实例化,类模板在引用实际模板类类型时实例化,函数模板在调用它或用它对函数指针进行初始化或赋值时实例化,类模板的每次实例化都会产生一个独立的类类型
用模板类定义的类型总是包含模板实参,使用函数模板时,编译器通常会为我们推断模板实参
16.2.1 模板实参推断
要确定应该实例化哪个函数,编译器会查看每个实参,如果相应形参声明为类型形参的类型,则编译器从实参的类型推断形参的类型,从 函数实参 确定 模板实参的类型和值 的过程叫做模板实参推断
模板形参可以用作一个以上函数形参的类型,在这种情况下,模板类型推断必须为每个对应的函数实参产生相同的模板实参类型,如果推断的类型不匹配,则调用将会出错,如果想要允许实参的常规转换,则函数必须用两个以上类型形参来定义
一般而言,不会转换实参以匹配已有的实例化,相反,会产生新的实例,除了产生新的实例之外,编译器只会执行两种转换:const转换:接受const引用或const指针的函数可以分别用非const对象的引用或指针来调用,无须产生新的实例化;数组或函数到指针的转换:如果模板形参不是引用类型,则对数组或函数类型的实参应用常规指针转换,类型转换限制只适用于类型为模板形参的那些实参
可以使用函数模板对函数指针进行初始化或赋值,编译器使用指针的类型实例化具有适当模板实参的模板版本,获取函数模板实例化的地址的时候,上下文必须是这样的: 它允许为每个模板形参确定唯一的类型或值
16.2.2 函数模板的显式实参
在某些情况下,不可能推断模板实参的类型,当函数的返回类型必须与形参表中所用的所有类型都不同时,最常出现这一问题,在这种情况下,有必要覆盖模板实参推断机制,并显式指定为模板形参所用的类型或值
16.3 模板编译模型
当编译器看到模板定义的时候,它不立即产生代码,只有看到用到模板时,如调用了函数模板或调用了类模板的对象的时候,编译器才产生特定类型的模板实例一般而言,当调用函数的时候,编译器只需要看到函数的声明,类似的,定义类类型的对象时,类定义必须可用,但成员函数的定义不是必须存在的,因此,应当将类定义和函数声明放在头文件中,而普通函数和类成员函数的定义放在源文件中,但是,模板不同,要进行实例化,编译器必须能够访问定义模板的源代码,当调用函数模板或类模板的成员函数的时候,编译器需要函数定义,需要那些通常放在源文件中的代码
“包含”模型在VS的实现:
在头文件中(.h)或在源文件中(.cpp)加文件保护(#ifndef XXX_X_ #def XXX_X_ #endif),防止重复定义,头文件尾部包含源文件(#include XXX.cpp),源文件(模板成员函数的实现)不能添加在工程中,即从工程中移除(是保留文件,不是删除文件),防止文件“分别编译”
其他实现方法:将类的定义和实现全部放在头文件中,省略源文件 或者 分开头文件和源文件,在main中包含源文件
16.4 类模板成员
通常,当使用类模板的名字的时候,必须指定模板形参,这一规则有个例外:在类本身的作用域内部,可以使用类模板的非限定名,编译器不会为类中使用的其他模板的模板形参进行这样的推断,因此,必须为伙伴类指定类型形参
16.4.1 类模板成员函数
类模板的成员函数本身也是函数模板,在实例化类模板成员函数的时候,编译器不执行模板实参推断,类模板成员函数的模板形参由调用该函数的对象的类型确定,对象的模板实参能够确定成员函数模板形参,用模板形参定义的函数形参的实参允许进行常规转换,类模板的成员函数只有为程序所用才进行实例化,类模板的指针定义不会对类进行实例化,只有用到这样的指针时才会对类进行实例化
16.4.2 非类型形参的模板实参
类模板的非类型形参,使用时必须显式声明形参值,非类型模板实参必须是编译时常量表达式
16.4.3 类模板中的友元声明
在类模板中可以出现三种友元声明,每一种都声明了一个或多个实体的友元关系:普通非模板类或函数的友元声明,将友元关系授予明确指定的类或函数;类模板或函数模板的友元声明,授予对友元所有实例的访问权;只授予对类模板或函数模板的特定实例的访问权的友元声明
声明依赖性:当授予对给定模板的所有实例的访问权的时候,在作用域中不需要存在该类模板或函数模板的声明,实质上,编译器将友元声明也当作类或函数的声明对待;想要限制对特定实例化的友元关系时,必须在可以用于友元声明之前声明类或函数,如果没有事先告诉编译器该友元是一个模板,则编译器将认为该友元是一个普通非模板类或非模板函数
16.4.4 Queue和QueueItem的友元声明
将类模板设为友元:对于实例化的Queue类的每种类型,我们想要Queue类和QueueItem类之间的一对一映射;将函数模板设为友元:输出操作符需要成为Queue类和QueueItem类的友元,Queue类的输出operator<<依赖于item对象的operator<<实际输出每个元素
16.4.5 成员模板
任意类,可以拥有 本身为类模板 或 函数模板的成员,这种成员称为成员模板,成员模板不能为虚
16.4.6 完整的Queue类
16.4.7 类模板的static成员
类模板可以像任意其他类一样声明static成员,每个实例化表示截然不同的类型,所以给定实例化的所有对象都共享一个static成员,可以通过类类型的对象访问类模板的static成员,或者通过使用作用域操作符直接访问成员,必须在类外出现数据成员的定义,成员定义必须指出它是模板类型的成员:像定义在类外部的任意其他类成员一样定义,它用关键字template开头,后面接着类模板的形参表和类名
代码(成员模板,友元函数):
#include <iostream> using namespace std; struct B{ int num; B(): num(100) { } friend ostream& operator << (ostream& o, const B& b ); //const B& b 友元函数不属于B }; ostream& operator << (ostream& o, const B& b ){ o << b.num; return o; } template <class T1> //友元函数前置声明 class A; template <class T1> //友元函数前置声明 void func1( const A<T1>& a); template <class T1> class A{ T1 data; public: void setData(const T1& data){ //const T1& this->data = data; } template<class T2> void func(const T2& t2, const T1& t1)const; //成员模板 const friend void func1<T1>( const A<T1>& a); //友元函数 func1<T1> }; template<class T1> template<class T2> void A <T1>:: func(const T2& t2, const T1& t1)const { cout << t2 << endl; //const T2& cout << data << endl; //T1 成员模板实例化是成员函数,可以访问类模板实例的数据 cout << t1 << endl; //const T1& } template <class T1> //友元类外声明 void func1( const A<T1>& a){ cout << a.data << endl; } int main() { B b; //T2 int x = 300; //T1 double y = 1.23456; A< int > a; //T1 类模板实例化 a.setData( x );//T1& a.func( b,y ); //T2& 成员模板实例化 double 强转为 T1 func1(a); return 0; }
数组模板类 重载 << 运算符 :
#include <iostream> #include <string> using namespace std; //<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< struct A{ int num; A(int num=100): num(num) { } friend ostream& operator << (ostream& o, const A& a ); }; ostream& operator << (ostream& o, const A& a ){ o << a.num; return o; } //<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< //友元前置声明 template<typename T,int N> class B; template<typename T,int N> ostream& operator << ( ostream& o, const B<T,N>& b ); //两个参数 template<typename T,int N> struct B{ T a[N]; //数组 B():a{ T() }{ } //无参构造 T类型的默认构造 T() 初始化 B( const T (&arr)[N] ){ //有参构造 for(int i=0;i<N; ++i ){ a[i] = arr[i]; } } friend ostream& operator << <T> ( ostream& o, const B<T,N>& b ) ; }; template<typename T,int N> //模板形参表 ostream& operator << (ostream& o, const B<T,N>& b ){ for(int i=0;i<N; ++i ){ o<< b.a[i] << ","; } return o; } int main() { int arr[5] = { 1,2,3,4,5 }; double arr1[5] = {1.1, 2.2, 3.3, 4.4, 5.5 }; A arr2[5]; //类类型数组 A类默认构造num为100 B<int,5> b(arr); //B b(arr); 可以推出arr的类型及大小 B<double,5> b1(arr1); //B b1(arr); 同上 B<int,5> b0; // 默认构造 B<A,5> a(arr2); //B a(arr2); 同上 B<A,5> a1; // 默认构造 cout << b << endl; cout << b1 << endl; cout << b0 << endl; // 默认构造 cout << a << endl; cout << a1 << endl; // 默认构造 string s[] = {"hello","world"}; B<string,2> bs(s); //B bs(s); 同上 cout << bs << endl; B<string,3> bs1; cout << bs1 << endl; return 0; }
16.5 一个泛型句柄类
句柄类管理继承层次中对象的指针,句柄的用户不必管理指向这些对象的指针,用户代码可以使用句柄类来编写,句柄能够动态分配和释放相关继承类的对象,并且将所有实际工作转发给继承层次中的底层类
16.5.1 定义句柄类
Handle类的行为类似于指针:复制Handle对象将不会复制基础对象,复制之后,两个Handle对象将引用同一基础对象,要创建Handle对象,用户需要传递属于由Handle管理的类型(或从该类型派生的类型)的动态分配对象的地址,Handle将拥有这个对象,而且,一旦不再有任意Handle对象与该对象关联,Handle类将负责删除该对象
#include <iostream> using namespace std; template<class T> class Handle{ private: T* ptr; //指向基础对象的指针 size_t *use; //指针计数 //即多少个指针指向同一个基础对象 void rem_ref(){ //删除基础对象(根据计数判断是否删除) if(--*use == 0){ delete ptr; //删除基础对象 delete use; //删除计数 } } public: Handle(T *p ):ptr(p ),use(new size_t(1) ){ } //指针ptr指向动态分配的基础对象的地址 Handle(const Handle& h):ptr(h.ptr ), use(h.use) { ++*use; } //复制,++*use Handle& operator=(const Handle& rhs ); ~Handle() { rem_ref(); } //Handle对象析构 // 删除基础对象(根据计数判断是否删除) //用于访问基础对象 T& operator*(); //解引用操作符 T* operator->(); //成员操作符 const T& operator*()const; const T* operator->()const; }; template<class T> inline Handle<T>& Handle<T>:: operator=(const Handle& rhs) { ++*rhs.use; //protect against self-assignment //防止自我复制 rem_ref(); //decrement use count and delete pointers if needed ptr = rhs.ptr; use = rhs.use; return *this; } template<class T> inline T& Handle<T>::operator*() { if(ptr) return *ptr; throw std::runtime_error("dereference of unbound Handle"); } template<class T> inline T* Handle<T>::operator->() { if(ptr) return ptr; throw std::runtime_error("dereference of unbound Handle"); } template<class T>//const Handle对象可以调用,返回类型是const T&不可以修改基础对象 inline const T& Handle<T>::operator*()const { if(ptr) return *ptr; throw std::runtime_error("dereference of unbound Handle"); } template<class T> inline const T* Handle<T>::operator->()const { if(ptr) return ptr; throw std::runtime_error("dereference of unbound Handle"); } struct A{ int x; A():x(128 ){ } virtual~A(){ cout << "~A()"; } //virtual }; struct B:public A{ ~B(){ cout << "~B()"; } }; int main() { /* A* p = new B(); //如何保证一个对象只初始化一个句柄! Handle<A> h(p ); Handle<A> h1(p ); */ //用动态对象初始化句柄! Handle<A> h(new B() ); Handle<A> h1(new A() ); cout << "Here" << endl; return 0; }
16.5.2 使用句柄
可以通过用句柄类模板实例Handle<Item_base>对象代替Item_base指针并删去复制控制成员
class Sales_item{ Handle<Item_base > h; //传递给构造函数的Item_base对象的副本上的Handle对象 public: Sales_item():h() { } Sales_item(const Item_base& item):h(item.clone() ){ } //数据成员没有指针,使用合成复制控制成员函数// //数据成员访问操作 const Item_base* operator->() const { h.operator->(); } //返回基础对象指针 const Item_base& operator* () const { *h; } //返回基础对象 };
基于Handle的Sales_item版本,有一个数据成员,该数据成员是关联传递给构造函数的Item_base对象的副本上的Handle对象,因为Sales_item的这个版本没有指针成员,所以不需要复制控制成员,Sales_item的这个版本可以完全地使用合成的复制控制成员,管理使用计数和相关Item_base对象的工作在Handle内部完成
句柄类模板及原Sales_item类代码
//************************************************// class Sales_item{ //绑定Item_base类型的对象并使用* ->操作符执行Item_base的操作//句柄类 Item_base *p; std::size_t *use; void decr_use(){ if(--*use == 0 ) { delete p; delete use; } } public: Sales_item():p(0),use(new std::size_t(1) ) { } Sales_item(const Item_base& item):p(item.clone()),use(new std::size_t(1) ){ } //绑定一个Item_base对象的副本 //复制控制三成员函数 Sales_item(const Sales_item &i):p(i.p ), use(i.use ) { ++*use; } //复制构造 Sales_item& operator = (const Sales_item& ); //赋值运算符重载 ~Sales_item() { decr_use(); }; //析构 //数据成员访问操作 const Item_base* operator->() const{ if(p) return p; else throw std::logic_error("unbound /Sales_item"); } const Item_base& operator* () const { if(p) return *p; else throw std::logic_error("unbound /Sales_item"); } }; //*****************************************************// template<class T> class Handle{ private: T* ptr; //指向基础对象的指针 size_t *use; //指针计数 //即多少个指针指向同一个基础对象 void rem_ref(){ //删除基础对象(根据计数判断是否删除) if(--*use == 0){ delete ptr; //删除基础对象 delete use; //删除计数 } } public: Handle(T *p = 0 ):ptr(p ),use(new size_t(1) ){ } //指针ptr指向动态分配的基础对象的地址 //复制控制三成员函数 Handle(const Handle& h):ptr(h.ptr ), use(h.use) { ++*use; } //复制,++*use Handle& operator=(const Handle& rhs ); ~Handle() { rem_ref(); } //Handle对象析构 // 删除基础对象(根据计数判断是否删除) //用于访问基础对象 T& operator*(); //解引用操作符 T* operator->(); //成员操作符 const T& operator*()const; const T* operator->()const; }; template<class T> inline Handle<T>& Handle<T>:: operator=(const Handle& rhs) { ++*rhs.use; //protect against self-assignment //防止自我复制 rem_ref(); //decrement use count and delete pointers if needed ptr = rhs.ptr; use = rhs.use; return *this; } template<class T> inline T& Handle<T>::operator*() { if(ptr) return *ptr; throw std::runtime_error("dereference of unbound Handle"); } template<class T> inline T* Handle<T>::operator->() { if(ptr) return ptr; throw std::runtime_error("dereference of unbound Handle"); } template<class T>//const Handle对象可以调用,返回类型是const T&不可以修改基础对象 inline const T& Handle<T>::operator*()const { if(ptr) return *ptr; throw std::runtime_error("dereference of unbound Handle"); } template<class T> inline const T* Handle<T>::operator->()const { if(ptr) return ptr; throw std::runtime_error("dereference of unbound Handle"); }
16.6 模板特化
我们并不总是能够写出对所有可能被实例化的类型都最合适的模板,某些情况下,通用模板定义对于某个类型可能是完全错误的,通用模板定义也许不能编译或做错误的事情;另外一些情况下,可以利用关于类型的一些特殊知识,编写比模板实例化来的函数更有效率的函数
16.6.1 函数模板的特化
模板特化:一个或多个模板形参的实际类型或实际值是指定的,特化的形式如下:关键字template后面接一对空的尖括号<>;再接模板名和一对尖括号,尖括号中指定这个特化定义的模板形参;函数形参表;函数体
#include <iostream> using std::cout; using std::endl; template<typename T, int N> void f( const T (&arr)[N] ){ for(int i=0; i<N; ++i){ cout << arr[i] << ","; } } template<> //模板特化 //非类型形参怎么办! void f<char>( const char(&arr)[6] ){ // cout << arr; } template<int N> //比其他的模板更特例化 void f( const char(&arr)[N] ){ //非类型形参 cout << arr; } int main() { int arr[] = {1,2,3}; f( arr ); char word[] ="Hello" ; /* const char* str[1] ={word } ; //包装c格式字符串//不用特例化 f(str ); */ f(word ); //非类型形参函数模板实例化 return 0; }
特化的声明必须与对应的模板相匹配,声明模板特化:函数模板特化可以声明而无须定义,模板特化声明看起来与定义很像,但省略了函数体,如果可以从函数形参表推断模板实参,则不必显式指定模板实参,特化版本的时候,对实参类型不应用转换,实参类型必须与特化版本的函数的形参类型完全匹配,在能够声明或定义特化之前,它所特化的模板的声明必须在作用域中
16.6.2 类模板的特化
整个类特例化:特化可以定义与模板本身完全不同的成员,类模板成员的定义不会用于创建显式特化成员的定义,在类外特化外部成员时,成员之前不能加template<>标记,我们的类只在类的外部定义了一个成员
16.6.3 特化成员而不特化类
可以只特例化某些成员,而不特化类,特化声明:成员特化的声明与任何其他函数模板特化一样,必须以空的模板形参表开头template<>
16.6.4 类模板的部分特化
如果类模板有一个以上的模板形参,也许想要特化某些模板形参而非全部,使用类模板的部分特化可以做到,类模板的部分特化本身也是模板,部分特化的模板形参表是对应的类模板定义形参表的子集,部分特化与对应模板有相同的名字,类模板的名字后面必须接着模板实参列表,部分特化的定义与通用模板的定义完全不会冲突,部分特化可以具有与通用类模板完全不同的成员集合,类模板成员的通用定义永远不会用来实例化模板部分特化的成员
16.7 重载与函数模板
函数模板可以重载:可以定义有相同名字但形参数目或类型不同的多个函数模板,也可以定义与函数模板有相同名字的普通非模板函数,当然,声明一组重载的函数模板不保证可以成功调用它们,重载的函数模板可能会导致二义性,定义函数模板特化几乎是比使用非模板版本更好
如果重载函数中既有普通函数又有函数模板,确定函数调用的步骤如下:为这个函数名建立候选函数集合;确定哪些普通函数是可行的;如果需要转换来进行调用,根据转换的种类排列可行函数,记住:调用模板函数的实例所允许的转换是有限的;重新排列去掉函数模板实例的可行函数(如果只有一个函数可选,就调用这个函数,否则,调用有二义性)