目录
(2)类模板实例化(从通用的类模板定义中生成类的过程称为模板实例化)
导入:
今天编程时碰到一个问题,写了一个单例的类模板,之前程序一直能跑,但今天使用了其中一个函数时却报错。后来发现是自己忘记写结束符号了。这就引出我知识点的不足了:之前为啥能跑正常?错误一只存在,为啥总是能编译通过?类中的函数何时才被实例化?类何时实例化?实例化的类是不是所有的函数都同时被实例化?
在我们使用类模板时,只有当代码中使用了类模板的一个实例的名字,而且上下文环境要求必须存在类的定义时,这个类模板才被实例化。
- 声明一个类模板的指针和引用,不会引起类模板的实例化,因为没有必要知道该类的定义;
- 定义一个类类型的对象时需要该类的定义,因此类模板会被实例化;
- 在使用sizeof()时,它是计算对象的大小,编译器必须根据类型将其实例化出来,所以类模板被实例化;
- new表达式要求类模板被实例化;
- 引用类模板的成员会导致类模板被编译器实例化;
- 需要注意的是,类模板的成员函数本身也是一个模板。标准C++要求这样的成员函数只有在被调用或者取地址的时候,才被实例化。用来实例化成员函数的类型,就是其成员函数要调用的那个类对象的类型。
<1>类模板定义及实例化
(1)定义一个类模板
template<class 模板参数表>
class 类名
{
// 类定义...
}
其中,template 是声明类模板的关键字,表示声明一个模板,模板参数可以是一个,也可以是多个;可以是类型参数 ,也可以是非类型参数。类型参数由关键字class或typename及其后面的标识符构成。非类型参数由一个普通参数构成,代表模板定义中的一个常量。
注意:
- 如果在全局域中声明了与模板参数同名的变量,则该变量被隐藏掉;
- 模板参数名不能被当作类模板定义中类成员的名字;
- 同一个模板参数名在模板参数表中只能出现一次;
- 在不同的类模板或声明中,模板参数名可以被重复使用;
typedef string type; template<class type,int width> class Graphics { type node;//node不是string类型 typedef double type;//错误:成员名不能与模板参数type同名 }; template<class type,class type>//错误:重复使用名为type的参数 class Rect; template<class type> //参数名”type”在不同模板间可以重复使用 class Round;
- 在类模板的前向声明和定义中,模板参数的名字可以不同;
// 两个 Image 声明都引用同一个类模板的声明 template <class T> class Image; template <class U> class Image; // 模板的真正定义 template <class Type> class Image { //模板定义中只能引用名字”Type”,不能引用名字”T”和”U” }
- 类模板参数可以有缺省实参,给参数提供缺省实参的顺序是先右后左;
- 类模板名可以被用作一个类型指示符。当一个类模板名被用作另一个模板定义中的类型指示符时,必须指定完整的实参表; (在类模板自己的定义中不需指定完整模板参数表)
template<class type>
class Graphics
{
Graphics *next;//在类模板自己的定义中不需指定完整模板参数表
};
template <calss type>
void show(Graphics<type> &g)
{
Graphics<type> *pg=&g;//必须指定完整的模板参数表
}
(2)类模板实例化(从通用的类模板定义中生成类的过程称为模板实例化)
类模板什么时候会被实例化呢?
- 当使用了类模板实例的名字,并且上下文环境要求存在类的定义时;
- 对象类型是一个类模板实例,当对象被定义时,此点被称作类的实例化点;
- 一个指针或引用指向一个类模板实例,当检查这个指针或引用所指的对象时;
template<class Type>
class Graphics{};
void f1(Graphics<char>);// 仅是一个函数声明,不需实例化
class Rect
{
Graphics<double>& rsd;// 声明一个类模板引用,不需实例化
Graphics<int> si;// si是一个Graphics类型的对象,需要实例化类模板
}
int main()
{
Graphcis<char>* sc;// 仅声明一个类模板指针,不需实例化
f1(*sc);//需要实例化,因为传递给函数f1的是一个Graphics<char>对象
int objSize=sizeof(Graphics<string>);
//需要实例化,因为sizeof会计算Graphics<string>对象的大小,为了计算大小,编译器必须根据类模板定义产生该类型
}
(3)非类型参数的模板实参
要点:
- 绑定给非类型参数的表达式必须是一个常量表达式;
- 从模板实参到非类型模板参数的类型之间允许进行一些转换。包括左值转换、限定修饰转换、提升、整值转换;
- 可以被用于非类型模板参数的模板实参的种类有一些限制;
<示例代码>
Template<int* ptr> class Graphics{......};
Template<class Type,int size> class Rect{......};
const int size=1024;
Graphics<&size> bp1;//错误:从const int*->int*是错误的。
Graphics<0> bp2; //错误不能通过隐式转换把0转换成指针值
const double db=3.1415;
Rect<double,db> fa1;//错误:不能将const double转换成int.
unsigned int fasize=255;
Rect<String, fasize> fa2;//错误:非类型参数的实参必须是常量表达式,将unsigned改为const就正确。
Int arr[10];
Graphics<arr> gp;//正确
<2>类模板的成员函数
要点:
- 类模板的成员函数可以在类模板的定义中定义(inline函数),也可以在类模板定义之外定义(此时成员函数定义前面必须加上template及模板参数);
- 类模板成员函数本身也是一个模板,类模板被实例化时它并不自动被实例化,只有当它被调用或取地址,才被实例化;
<示例代码>
template<class type>
Class Graphics{
Graphics(){…}//成员函数定义在类模板的定义中
void out();
};
template<class type>//成员函数定义在类模板定义之外
void Graphics<type>::out(){…}
<3>类模板的友元声明
类模板中可以有三种友元声明:
(1)非模板友元类或友元函数
<示例代码>
class Graphics{void out();};
Template<class T>
Class Rect{
friend class Graphics;//类Graphics、函数create、out是类模板Rect所有实例的友元
friend void create();
friend void Graphics::out();
};
(2)绑定的友元类模板或函数模板
(3)非绑定的友元类模板或函数模板
第二种声明表示类模板的实例和它的友元之间是一种一对一的映射关系:
如图:
<示例>绑定的友元模板
template<class type>
void create(Graphics<type>);
template<class type>
class Graphics{
friend void create<type>(Graphics<type>);
};
第三种声明表示类模板的实例和它的友元之间是一种一对多的映射关系:
如图:
<示例>非绑定的友元模板
template<class type>
class Graphics
{
template<class T>
friend void create(Graphics<T>);
};
注意:当把非模板类或函数声明为类模板友元时,它们不必在全局域中被声明或定义,但将一个类的成员声明为类模板友元,该类必须已经被定义,另外在声明绑定的友元类模板或函数模板时,该模板也必须先声明。
<错误示例>
template <class T>
class A {
private:
friend class B<T>; //错误:类B必须先声明
};
template <class T>
class B{};
<4>类模板的静态数据成员、嵌套类型
(1)类模板的静态数据成员
要点:
- 静态数据成员的模板定义必须出现在类模板定义之外;
- 类模板静态数据成员本身就是一个模板,它的定义不会引起内存被分配,只有对其实例化才会分配内存;
- 当程序使用静态数据成员时,它被实例化。每个静态成员实例都与一个类模板实例相对应,静态成员的实例引用要通过一个类模板实例;
template<class type>
class Graphics
{
static Graphics *next;
static const type item;
};
template<class type>
Graphics<type> * Graphics<type>::next=0;
template<class type>
type Graphics<type>::item=NULL;
//静态成员定义分为两部分:前一部分是类型,比如Graphics<type>*,后一部分是名称和值,比如Graphics<type>::next=0;
(2)类模板的嵌套类型
要点:
- 在类模板中允许再嵌入模板,因此类模板的嵌套类也是一个模板,它可以使用外围类模板的模板参数;
- 当外围类模板被实例化时,它不会自动被实例化,只有当上下文需要它的完整类类型时,它才会被实例化;
- 公有嵌套类型可以被用在类定义之外,这时它的名字前必须加上类模板实例的名字;
template<class type>
class Graphics{
public:
template<class T>
class Rect{void out(type a,T b);};
};
Graphics<int>::Rect<double> node;//引用公有嵌套类型必须加上类模板实例名字
<5>成员模板
定义:成员定义前加上template及模板参数表。
要点:
- 在一个类模板中定义一个成员模板,意味着该类模板的一个实例包含了可能无限多个嵌套类和无限多个成员函数;
- 只有当成员模板被使用时,它才被实例化;
- 成员模板可以定义在其外围类或类模板定义之外;
<示例代码>
template<class type>
class Graphics<type>
{
public:
template<class T>
class Rect{void out(type a,T b);};
};
template<class Gtype> template<class TT>
void Graphics<Gtype>::Rect<TT>::out(Gtype a,TT b){}//成员模板被定义在类模板定义之外(要跟上完整模板实参)
Graphics<int>的实例可能包括下列嵌套类型:
Graphics<int>::Rect<double>
Graphics<int>::Rect<string>
注意:类模板参数不一定与类模板定义中指定的名字相同
<6>类模板的编译模式
(1)包含编译模式
这种编译模式下,类模板的成员函数和静态成员的定义必须被包含在“要将它们实例化”的所有文件中,如果一个成员函数被定义在类模板定义之外,那么这些定义应该被放在含有该类模板定义的头文件中。
(2)分离编译模式
这种模式下,类模板定义和其inline成员函数定义被放在头文件中,而非inline成员函数和静态数据成员被放在程序文本文件中。
//------Graphics.h---------
export template<class type>
class Graphics
{
void Setup(const type &);
};
//-------Graphics.c------------
#include “Graphics.h”
template <class type>
Void Graphics<type>::Setup(const type &){…}
//------user.c-----
#include “Graphics.h”
void main()
{
Graphics<int> *pg=new Graphics<int>;
int ival=1;
//Graphics<int>::Setup(const int &)的实例(下有注解)
Pg->Setup(ival);
}
Setup的成员定义在User.c中不可见,但在这个文件中仍可调用模板实例Graphics<int>::Setup(const int &)。为实现这一点,须将类模声明为可导出的:当它的成员函数实例或静态数据成员实例被使用时,编译器只要求模板的定义,它的声明方式是在关键字template前加关键字export。
(3)显式实例声明
当使用包含编译模式时,类模板成员的定义被包含在使用其实例的所有程序文本文件中,何时何地编译器实例化类模板成员的定义,我们并不能精确地知晓,为解决这个问题,标准C++提供了显式实例声明:关键字template后面跟着关键字class以及类模板实例的名字。
<例>
#include “Graphics.h”
template class Graphics<int>;//显式实例声明
显式实例化类模板时,它的所有成员也被显式实例化!
<7>类模板的特化及部分特化
(1)类模板的特化
<示例>
template<class type>
Class Graphics
{
Public:void out(type figure)
{
…
}
};
Class Rect{…};
template<>
void Graphics<Rect>::out(Rect figure){…}
如果模板实参是Rect类型,我们不希望使用类模板Graphics的通用成员函数定义,来实例化成员函数out(),我们希望专门定义Graphics<Rect>::out()实例,让它使用Rect里面的成员函数。为此我们可以通过一个显示特化定义,为类模板实例的一个成员提供一个特化定义。
格式:template<> 成员函数特化定义
下面为类模板实例Graphics<Rect>的成员函数out()定义了显式特化:
template<>
void Graphics<Rect>::out(Rect figure){…}
注意:
- 只有当通用类模板被声明后,它的显式特化才可以被定义;
- 若定义了一个类模板特化,则必须定义与这个特化相关的所有成员函数或静态数据成员,此时类模板特化的成员定义不能以符号template<>作为打头。(template<>被省略);
- 类模板不能够在某些文件中根据通用模板定义被实例化,而在其他文件中却针对同一组模板实参被特化;
(2)类模板的三种特化 类型
类模板特化的几种类型:一是特化为绝对类型; 二是特化为引用,指针类型;三是特化为另外一个类模板。
//general version
template<classT>
class Compare
{
public :
static bool IsEqual( const T& lh, const T& rh)
{
return lh == rh;
}
};
①特化为绝对类型
也就是说直接为某个特定类型做特化,这是我们最常见的一种特化方式, 如特化为float, double等
// specialize for float
template <>
class Compare<float>
{
public:
static bool IsEqual( const float& lh, const float& rh)
{
return abs(lh - rh) < 10e - 3;
}
};
②特化为引用/指针类型
stl源码中iterator_traits的特化如下:
template <class _Iterator>
struct iterator_traits {
typedef typename _Iterator::iterator_category iterator_category;
typedef typename _Iterator::value_type value_type;
typedef typename _Iterator::difference_type difference_type;
typedef typename _Iterator::pointer pointer;
typedef typename _Iterator::reference reference;
};
// specialize for _Tp*
template <class _Tp>
struct iterator_traits < _Tp *> {
typedef random_access_iterator_tag iterator_category;
typedef _Tp value_type;
typedef ptrdiff_t difference_type;
typedef _Tp * pointer;
typedef _Tp & reference;
};
当然,除了T*, 我们也可以将T特化为 const T*, T&, const T&等,以下还是以T*为例:
//specialize for T*
template <class T>
class Compare <T*>
{
public :
static bool IsEqual( const T* lh, const T* rh)
{
return Compare<T> ::IsEqual( *lh, *rh);
}
};
这种特化其实是就不是一种绝对的特化, 它只是对类型做了某些限定,但仍然保留了其一定的模板性,这种特化给我们提供了极大的方便, 如这里, 我们就不需要对int*, float*, double*等等类型分别做特化了。
③特化为另外一个类模板
这其实是第二种方式的扩展,其实也是对类型做了某种限定,而不是绝对化为某个具体类型,如下:
//specialize for vector<T>
template < class T >
class Compare <vector<T>>
{
public :
static bool IsEqual(const vector<T>& lh, const vector<T>& rh)
{
if (lh.size() != rh.size())
return false ;
else
{
for (int i = 0 ; i < lh.size(); ++ i)
{
if (lh[i] != rh[i]) return false ;
}
}
return true ;
}
};
这就把IsEqual的参数限定为一种vector类型, 但具体是vector<int>还是vector<float>, 我们可以不关心, 因为对于这两种类型,我们的处理方式是一样的,我们可以把这种方式称为“半特化”。
(3)类模板部分特化
如果模板有一个以上的模板参数,则有些人就可能希望为一个特定的模板实参或者一组模板实参特化类模板,而不是为所有的模板参数特化该类模板。即希望提供这样一个模板:它仍然是一个通用的模板,只不过某些模板参数已经被实际的类型或值取代。通过使用类模板部分特化可以实现这一点。
<示例>
template<int hi,int wid>
Class Graphics{…};
template<int hi>//类模板的部分特化
Class Graphics<hi,90>{…};
注意:
- 部分特化的模板参数表只列出模板实参仍然未知的那些参数;
- 类模板部分特化是被隐式实例化的。编译器选择“针对该实例而言最为特化的模板定义”进行实例化,当没有特化可被使用时,才使用通用模板定义;
例:Graphics<24,90> figure; 它即能从通用类模板定义被实例化,也能从部分特化的定义被实例化,但编译器选择的是部分特化来实例化模板。
- 类模板部分特化必须有它自己对成员函数、静态数据成员和嵌套类的定义;
<8>名字空间和类模板
类模板定义也可以被放在名字空间中。例如:
Namespace cplusplus_primer
{
template<class type>
class Graphics{…};
Template<class type>
type create()
{…}
}
当类模板名字Graphics被用在名字空间之外时,它必须被名字空间名cplusplus_primer限定修,或者通过一个using声明或指示符被引入。例如:
void main()
{
using cplusplus_primer::Graphics;
Graphics<int> *pg=new Graphics<int>;
}
注意:在名字空间中声明类模板也会影响该类模板及其成员的特化和部分特化声明的方式,类模板或类模板成员的特化声明必须被声明在定义通用模板的名字空间中(可以在名字空间之外定义模板特化)。