类模板是用来生成类的蓝图,与函数模板不同的是,编译器不能为类模板推断模板参数类型,所以为了使用类模板,需要在模板名后的尖括号中提供额外的信息。
定义类模板
类似函数模板,类模板以关键字template开始,后跟模板参数列表。在类模板(及其成员)的定义中,将模板参数当做替身,代替使用模板时用户需要提供的类型和值。
//实现一个类,用处是存放某种类型的元素,如string等
template <typename T> class Blob{
public:
typedef T value_type;
typedef typename std::vector<T>::size_type size_type;
//构造函数
Blob();
Blob(std::initializer_list<T> i1);
//相关函数
size_type size() const {return data->size;} //元素数目
bool empty() const {return data->empty();} //判断是否为空
//添加元素
void push_back(const T& t){data->push_back(t);}
//移动版本
void push_back(T&& t){data->pish_back(std::move(t));}
void pop_back();
//元素访问
T& back();
T& operator[](size_type i);
private:
std::shared_ptr<std::vector<T>> data;
//如果data[i]无效,则抛出msg
void check(size_type i,const std::string& msg) const;
};
在Blob模板中有一个名为T的模板类型参数,用来表示Blob保存的元素的类型,当实例化Blob时,T就会被替换成特定的模板实参类型。
实例化类模板:
当用上面的类定义一个类型时,需要提供元素类型:
Blob<int> a; //空Blob<int>
当编译器从Blob模板实例化出一个类的时候,他会重写Blob模板,将模板参数T的每个实例替换为给定的模板实参,每个实例都会形成一个独立的类,彼此之间也没有什么关联。
类模板的成员函数:
与其他类相同,可以在内部也可以在外部为其定义成员函数,且定义在内部的成员函数被隐式的声明为内联函数。
类模板的成员函数具有和模板相同的模板参数。因此,定义在类模板之外的成员函数就必须以关键字template开始,后接类模板参数列表。对应的Blob的成员函数在外部的定义可能是这样:
template <typename T>
bool Blob<int>::func(T i);
如果一个成员函数没有被使用到,那它不会被实例化,这一特性使得即使某种类型不能完全符合模板的要求,我们仍然能用该类型实例化类。
类模板内外使用类模板名:
在类模板内部使用类模板名时,可以直接使用模板名而不提供实参。编译器在处理模板自身引用时就如同用户已经提供了与模板参数匹配的实参一样,比如:
//在类中的返回该类类型的对象引用的函数可以直接写成
A& func();
//编译器会处理为类似下面的函数:
A<T>& func();
在类模板外定义其成员时,并不是在类的作用域中,直到遇到类名才表示进入类的作用域:
//递增
template <typename T>
Blob<T> Blob<T>::operator++(int){
Blob ret = *this; //保存当前值
++*this;
return *this;
}
由于返回类型位于类的作用域外,所以必须指出返回类型是一个实例化的Blob,它所用的类型和类实例化所用类型一致。在函数体类的时候是已经进入了类的作用域,因此无需提供模板实参,编译器将假定我们使用的类型与用户实例化所用的类型一致。
类模板和友元:
当一个类包含一个友元声明,类与友元是否是模板是各自相互无关的。即一个类模板如果包含一个非模板友元,则友元被授权可以访问所有模板实例;如果友元自身是模板,类可以授权给所有友元实例,也可以只授权给特定实例。
一对一友好关系:
类模板与另一个(类或函数)模板之间的友元关系最常见的是创建一一对应的方式,例如A类模板的友元是B类模板和一个相等运算符号函数:
//前置声明,在A类中声明友元需要
template <typename> class B;
template <typename> class A; //相等运算符函数中需要
template <typename T>
bool operator==(const A<T>&,const A<T>&);
template <typename T> class A{
friend class B<T>;
friend bool operator==(const A<T>&,const A<T>&);
//其它定义
......
};
这样在声明A的实例时:
A<char> a1; //B<char>和operator==<char>是本对象的友元
A<int> a2; //B<int>和operator==<int>是本对象的友元
//但是B<int>A的任何其它实例都没有特殊访问权限
通用和特定的模板友好关系:
一个类也可以将另一个模板的每个实例都声明为自己的友元,也可以只限定特定的实例为友元:
//前置声明,在将模板的一个特定实例声明为友元时要用到
template <typename T> class Pal;
class C{
friend class Pal<C>; //表示C实例化的Pal是C的友元
//Pal2的所有实例都是C的友元,这种情况就不需要前置声明
template <typename T> friend class Pal2;
};
template <typename T> class C2{
//C2的每个实例将相同是实例化的Pal声明为友元
friend class Pal<T>;
//Pal2的所有实例都是C2的所有实例的友元,不需要前置声明
//这里typename后跟的不是T,因为要让所有实例成为友元,模板参数应该不同
template <typename X> friend class Pal2;
//Pal3是一个非模板类,它是C2所有实例的友元,不需要前置声明
friend class Pal3;
};
令模板自己的类型成为友元:
在新标准中,可以将模板类型参数声明为友元:
template <typename T> class Bar{
friend T;
};
此处将用来实例化Bar的类型声明为友元,因此,对于某个类型名如类A,A将成为Bar<A>的友元。
类模板的static成员:
与其他任何类相同,类模板可以声明static成员:
template <typename T> class Foo{
public:
static std::size_t count(){return ctr;}
//其他成员
......
private:
static std::size ctr;
//其他成员
......
};
其中Foo是一个类模板,每个Foo的实例都有自己的static成员实例,即对给定类型的X,都有一个Foo<X>::ctr和Foo<X>::count,所有的对象都共享相同的ctr变量和count函数。
与其他static数据成员相同,模板类的每个static数据成员有且只有一个定义,但是类模板的每个实例都有独立的一个static对象,所以应该将static数据成员也定义为模板:
template <typename T>
size_t Foo<T>::ctr = 0;
与非模板类的静态成员相同,可以通过一个类类型对象来访问类模板的static成员,也可以直接用作用域运算符直接访问成员:
Foo<int> f;
auto c1 = Foo<int>::count(); //实例化Foo<int>::count
c1 = f.count(); //使用count函数
c1 = Foo::count(); //错误,不知道使用哪个版本实例的count