类模板与函数模板相似,可以有隐式实例化、显式实例化和显式具体化,统称具体化。模板以泛型的方式描述类,而具体化使用具体的类型生成类声明。
1.隐式实例化
目前本章所有模板示例都是用的此方法:它们声明一个或多个对象,指出所需的类型,而编译器使用通用模板提供的处方生成具体的类定义:
ArrayTP<int, 100> stuff;
编译器在需要对象之前不会生成类的隐式实例化:
ArrayTP<double, 30>* pt; //指针,还不需要对象
pt = new ArrayTP<double, 30>; //现在需要对象了
第二句导致编译器生成类定义,并根据定义创建对象。
2.显式实例化
当关键字template并指出所需类型来声明类时,编译器将生成类声明的显式实例化。声明必须位于模板定义所在的名称空间中。比如下面的声明将ArrayTP
template class ArrayTP<string, 100>;
这种情况下,虽然没有创建或提及类对象,编译器也将生成类声明(包括方法定义)。和隐式实例化一样,也将根据通用模板来生成具体化。
3.显示具体化
显式具体化是特定类型(用于替换模板中的泛型)的定义。有时候可能需要在为特殊类型实例化时,对模板进行修改,使其行为不同。在这种情况下,可以创建显式具体化。
举例:假设已经为用于表示排序后数组的类(元素在加入时被排序)定义了一个模板:
template <typename T>
class SortedArray
{
...
};
另外,假设模板使用>运算符来对值进行比较。对于数字这会管用;但如果T表示一种类,则只要定义T::operator>()方法,也会管用;但如果const char*表示的字符串,这就不管用了。实际上模板使可以正常工作的,但是字符串将按照地址(按字母顺序)排序。这要求类定义使用strcmp(),而不是>来比较。这时可以提供一个显式模板具体化,这将采用为具体类型定义的模板,而不是泛型定义的模板。当具体化模板和通用模板都与实例化请求匹配时,编译器将使用具体化版本。
具体化模板定义格式:
template <> class Classname<special-type-name> {...};
要使用新的表示法提供一个专供const char*类型使用的SortedArray模板,可以使用类似于下面的代码:
template <> class SortArray<const char char*>
{
...
};
其中的实现代码将使用strcmp()来比较数组值。
现在当请求const char*类型的SortArray模板时,编译器将使用上述专用的定义而不是通用模板的定义:
SortedArray<int> scores;
SortedArray<const char*> dates;
4.部分具体化
C++还允许部分具体化,即部分限制模板的通用性。
不如部分具体化可以给类型参数之一指定具体的类型:
template <class T1, class T2> class Pair {...};
template <class T1> class Pair<T1, int> {...};
关键字template后面的<>声明的是没有被具体化的类型参数。因此上述第二个声明将T2具体化为int,但T1保持不变。注意,如果指定所有的类型,则<>内将为空,这将导致显式具体化:
template <> class Pair<int, int> {...};
如果有多个模板可供选择,编译器将使用具体化程度最高的版本模板
Pair<double, double> p1; //使用通用模板
Pair<double, int> p2; //使用Pair部分具体化
Pair<int, int> p3; //使用Pair显式具体化
也可以通过为指针提供特殊版本来部分具体化现有的模板:
template<class T> //通用版本
class Feeb {...};
template<class T*> //指针部分具体化版本
class Feeb {...};
如果提供的类型不是指针,则编译器将使用通用版本;如果提供的是指针,则编译器将使用指针的具体化版本:
Feeb<char> fb1; //使用通用Feeb模板,T是char
Feeb<char*> fb2; //使用T*具体化,T还是char
如果没有进行部分具体化,则第二个声明将使用通用模板,则T转换为char*。如果进行了部分具体化,第二个声明将使用具体化模板,T转换为char。
比分具体化特性使得能够设置各种限制,比如:
//通用模板
template <class T1, class T2, class T3> class Trio {...};
//T3具体化为T2
template <class T1, class T2> class Trio<T1, T2, T2> {...};
//T3、T2具体化为T1*
template <class T1> class Trio<T1, T1*, T1*> {...};
有了上述声明后,编译器会做出如下抉择:
Trio<int, short, char*> t1; //使用通用模板
Trio<int, short> t2; //使用Trio
Trio<char, char*, char*> t3 //使用Trio
模板可用作结构、类或模板类的成员。要完全实现STL的设计,必须使用这项特性。接下来实现一个简短的模板类示例,该模板类将另一个模板类和模板函数作为其成员:
#include
using std::cout;
using std::endl;
template <typename T>
class beta
{
private:
template <typename V>
class hold
{
private:
V val;
public:
hold(V v = 0) : val(v) {}
void show() const { cout << val << endl; }
V Value() const { return val; }
};
hold<T> q;
hold<int> n;
public:
beta(T t, int i) : q(t), n(i) {}
template<typename U>
U blab(U u, T t) { return (n.Value() + q.Value()) * u / t; }
void Show() const { q.show(); n.show(); }
};
int main()
{
beta<double> guy(3.5, 3);
cout << "T was set to double\n";
guy.Show();
cout << "V was set to T, which is double, then V was set to int\n";
cout << guy.blab(10, 2.3) << endl;
cout << "U was set to int\n";
cout << guy.blab(10.0, 2.3) << endl;
cout << "U was set to double\n";
cout << "Done\n";
return 0;
}
hold模板是在私有部分声明的,因此只能在beta类中访问它。beta类使用hold模板声明了两个数据成员:
beta<T> q;
beta<int> n;
n是基于int类型的hold对象,而q是基于T类型(beta模板参数)的hold对象。在main()中下述声明使得T表示的是double,因此q类型为hold
:
beta<double> guy(3.5, 3);
blad()方法的U类型由该方法被调用时的参数值显式决定,T类型由对象的实例化类型确定。在这个例子中,guy的类型将T的类型设置为double,而下述方法调用的第一个参数将U的类型设置为int(10对应的类型):
cout << guy.blab(10, 2.5) << endl;
因此虽然混合类型将引起的自动类型转换导致blab()中的计算以double类型进行,但返回值的类型为U(即int),因此它被截断为28,如下面程序所示:
T was set to double
3.5
3
V was set to T, which is double, then V was set to int
28
U was set to int
28.2609
U was set to double
Done
主要调用guy.blab()时,使用10.0代替了10,因此U被设置为double,这使得返回类型为double,因此输出28.2608.
正如前面说明的,guy对象的声明将第二个参数的类型设置为double。与第一个参数不同的是,第二个参数的类型不是由函数调用设置的。比如下面的语句仍将blah()实现为blah(int, double),并根据常规函数原型规则将3转换为double
cout << guy.blah(10, 3) << endl;
可在beta模板中声明hold类和blah方法,并在beta模板的外面定义它们。但是有一些老编译器不接受模板成员,而另一些编译器接受模板成员,但不接受类外面定义。然而如果所用的编译器接受类外面定义,则在beta模板之外定义模板方法的代码如下:
template <typename T>
class beta
{
private:
template <typename V>
class hold;
hold<T> q;
hold<int> n;
public:
beta(T t, int i) : q(t), n(i) {}
template<typename U>
U blab(U u, T t);
void Show() const { q.show(); n.show();}
};
template <typename T>
template<typename V>
class beta<T>::hold
{
private:
V val;
public:
hold(V v = 0) : val(v) {}
void show() const { std::cout << val << std::endl; }
V Value() const { return val; }
};
template <typename T>
template <typename V>
U beta<T>::blab(U u, T t)
{
return (n.Value() + q.Value()) * u / t;
}
上述定义将T、V和U用作模板参数。因为模板是嵌套的,因此必须使用这种语法:
template <typename T>
template <typename V>
而不能是这种:
template<typename T, typename V>
定义还指出hold和blab是beta< T>类成员,这是通过使用作用域解析运算符来完成的。
模板可以包含类型参数(typename T)和非类型参数(如int n)。模板还可以包含本身就是模板的参数,这种参数是模板新增的特性, 用于实现STL。
stacktp头文件
#include
#include
#include "stacktp.h"
template <template <typename T> class Thing>
class Crab
{
private:
Thing<int> s1;
Thing<double> s2;
public:
Crab() {};
bool push(int a, double x) { return s1.push(a) && s2.push(x); }
bool pop(int &a, double &x) { return s1.pop(a) && s2.pop(x); }
};
int main()
{
using std::cout;
using std::cin;
using std::endl;
Crab<Stack> nebula;
int ni;
double nb;
cout << "Enter int double pairs, such as 4 3.5 (0 0 to end):\n";
while (cin >> ni >> nb && ni > 0 && nb > 0)
if (!nebula.push(ni, nb))
break;
while (nebula.pop(ni, nb))
cout << ni << ", " << nb << endl;
cout << "Done.\n";
system("pause");
return 0;
}
输出:
Enter int double pairs, such as 4 3.5 (0 0 to end):
50 22.48
25 33.87
60 19.12
0 0
60, 19.12
25, 33.87
50, 22.48
Done.
开头代码:
template <template <typename T> class Thing>
class Crab
//模板参数是template class Thing>
//其中template class Thing是类型
//Thing是参数
这意味着什么,假设有下面的声明:
Crab<King> legs;
为使上述声明被接收,模板参数King必须是一个模板类,其声明与模板参数Thing的声明匹配:
template <typename T>
class King {...};
在上面的程序代码中,Crab的声明声明了两个对象:
Thing<int> s1;
Thing<double> s2;
前面的legs声明将用King
替换成Thing
;用King
替换Thing
,然而上面的程序代码包含下面的声明:
Crab<Stack> nebula;
因此,Thing
将被实例化为Stack
,而Thing
将被实例化为Stack
。总之模板参数Thing将被替换为声明Crab对象时被用作模板参数的模板类型。
Crab类的声明对Thing代表的模板类做了另外3个假设,即这个类包含一个push()方法,包含一个pop()方法,且这些方法由特定的接口。Crab类可以使用任何与Thing类型声明匹配且包含方法push()和pop()的模板类。之前写过的stacktp恰好符合这种情况。
模板类也可以有友元。模板的友元分三种:
template <class T>
class HasFriend
{
public:
friend void counts();
...
};
上述声明将counts()函数成为模板所有实例化的友元。比如它会是类hasFriend
和HasFriend
的友元。
counts()函数不是通过对象调用的(它是友元,不是成员函数),也没有对象参数,那么它怎么访问HasFriend对象呢?有有很多可能性:它可以访问全局对象;也可以使用全局指针访问非全局对象;可以创建自己的对象;可以访问独立于模板类的静态数据成员。
假设要为友元函数提供模板类参数,能这样来进行友元声明吗?
friend void report(HasFriend &);
不可以!原因是不存在HasFriend这样的参数,而只有特定的具体化,如HasFriend
。要提供模板类参数,必须指明具体化,比如这样:
template <class T>
class HasFriend
{
friend void report(HasFriend<T> &); //约束模板友元
...
};
为理解上述代码的功能,想想声明一个特定类型的对象时,将生成的具体化:
HasFriend<int> hf;
编译器将用int替代模板参数T,因此友元声明的格式如下:
class HasFriend<int>
{
friend void report(HasFriend<int> &);
...
};
也就是说带HasFriend
参数的report()将成为HasFriend
类的友元。
同样带HasFriend<
参数的report()将是report()的一个重载版本——它是Hasfriend
类的友元。
注意report()本身不是模板函数,而只是使用一个模板作参数。这意味着必须为要使用的友元定义显式具体化:
void report(HasFriend<short> &) {...}; //short显式具体化
void report(HasFriend<int> &) {...}; //int显式具体化
接下来演示一遍。其中ct式静态成员,这意味着这个类的每一个特定的具体化都将有自己的静态成员。count()方法是所有HasFriend具体化友元,它report两个特定的具体化(HasFriend
和HasFriend
)的ct值。该程序还提供两个report(),分别是某个特定HasFriend具体化的友元。
#include
#include
using std::cout;
using std::endl;
template <typename T>
class HasFriend
{
private:
T item;
static int ct;
public:
HasFriend(const T &i) : item(i) {ct++;}
~HasFriend() {ct--;}
friend void counts();
friend void reports(HasFriend<T> &);
};
template <typename T>
int HasFriend<T>::ct = 0;
void counts()
{
cout << "int counts: " << HasFriend<int>::ct << "; ";
cout << "double counts: " << HasFriend<double>::ct << endl;
}
void reports(HasFriend<int> &hf)
{
cout << "HasFriend: " << hf.item << endl;
}
void reports(HasFriend<double> &hf)
{
cout << "HasFriend: " << hf.item << endl;
}
int main()
{
cout << "No objects declared: ";
counts();
HasFriend<int> hfi1(10);
cout << "After hfi1 declared: ";
counts();
HasFriend<int> hfi2(20);
cout << "After hfi2 declared: ";
counts();
HasFriend<double> hfdb(10.5);
cout << "After hfdb declared: ";
counts();
reports(hfi1);
reports(hfi2);
reports(hfdb);
system("pause");
return 0;
}
输出:
No objects declared: int counts: 0; double counts: 0
After hfi1 declared: int counts: 1; double counts: 0
After hfi2 declared: int counts: 2; double counts: 0
After hfdb declared: int counts: 2; double counts: 1
HasFriend<int>: 10
HasFriend<int>: 20
HasFriend<double>: 10.5
template <typename T> void counts();
template <typename T> void report(T &);
然后在函数中再次将模板声明为友元,这些语句根据类模板参数的类型声明具体化:
template <typename TT>
class HasFriendT
{
...
friend void counts<TT>();
friend void report<>(HasFriendT<TT> &)
};
声明中的<>指出这是模板具体化。对于report(),<>可以为空,因为可以从函数参数推断处如下模板类型参数:
HasFriendT<TT>
也可以这样:
report<HasFriend<TT> >(HasFriendT<TT> &)
但counts()函数没有参数,因此必须使用模板参数语法来指明其具体化。注意:TT是HasFriendT类的参数类型。
同样,理解这些声明的最佳方式也是设想声明一个特定具体化的对象时,它们将变成什么样。比如假设声明这样一个对象:
HasFriendT<int> squack;
编译器将用int替换TT,并生成下面的类定义:
class HasFriendT<int>
{
...
friend void counts<int>();
friend void report<>(HasFriendT<int> &)
};
基于TT的具体化将变成int,基于HasFriendT
的具体化将变成HasFriendT
。因此模板具体化counts
和report
将被声明为HasFriendT
类的友元。
程序必须满足的第三个要求是为友元提供模板定义。
下面的代码说明了这三个方面:
#include
#include
using std::cout;
using std::endl;
//模板原型
template <typename T> void counts();
template <typename T> void report(T &);
template <typename TT>
class HasFriendT
{
private:
TT item;
static int ct;
public:
HasFriendT(const TT &i) : item(i) {ct++;}
~HasFriendT() {ct--;}
friend void counts<TT>();
friend void report<>(HasFriendT<TT> &);
};
template <typename T>
int HasFriend<T>::ct = 0;
template <typename T> //模板友元函数定义
void counts()
{
cout << "template size: " << sizeof(HasFriendT<T>)<< "; ";
cout << "template counts(): " << HasFriendT<T>::ct << endl;
}
template <typename T>
void report(T &hf)
{
cout << hf.item << endl;
}
int main()
{
counts<int>();
HasFriendT<int> hfi1(10);
HasFriendT<int> hfi2(20);
HasFriendT<double> hfdb(10.5);
report(hfi1); //生成report(HasFriendT &)
report(hfi2); //生成report(HasFriendT &)
report(hfdb); //生成report(HasFriendT &)
cout << "counts() output:\n" ;
counts<int>();
cout << "counts() output:\n" ;
counts<double>();
system("pause");
return 0;
}
这里面的两个count()函数,它们分别是某个被实例化的类类型的友元。因为count()函数调用没有可被编译器用来推断出所需具体化的函数参数,所以这些调用使用count
和count
指明具体化。但对于report()调用,编译器可以从参数类型推断出要使用的具体化。使用<>格式也能获得同样的效果:
report<HasFriendT<int> >(hfi2); //相当于report(hfi2);
template <typename T>
class ManyFriend
{
...
template <typename C, typename D> friend void show2(C &, D&);
};
接下来演示使用非约束友元:
#include
#include
using std::cout;
using std::endl;
template<typename T>
class ManyFriend
{
private:
T item;
public:
ManyFriend(const T &i) : item(i) {}
template <typename C, typename D> friend void show2(C &, D &);
};
template <typename C, typename D> void show2(C &c, D &d)
{
cout << c.item << ", " << d.item << endl;
}
int main()
{
ManyFriend<int> hfi1(10);
ManyFriend<int> hfi2(20);
ManyFriend<double> hfdb(10.5);
cout << "hfi1, hfi2: ";
show2(hfi1, hfi2);
cout << "hfdb, hfi2: ";
show2(hfdb, hfi2);
system("pause");
return 0;
}
输出:
hfi1, hfi2: 10, 20
hfdb, hfi2: 10.5, 20
上述代码中,函数调用show2(hfi1, hfi2)与这个具体化匹配:
void show2<ManyFriend<int> &, ManyFriend<int> &>(ManyFriend<int> &c, ManyFriend<int> &d)
因为它是所有ManyFriend具体化的友元,所以能够访问所有具体化的item成员,但它只访问了ManyFriend
对象。
同样,show2(hfdb, hfi2)与这个具体化匹配:
void show2<ManyFriend<double> &, ManyFriend<int> &>(ManyFriend<double> &c, ManyFriend<int> &d)
它也是所有ManyFriend具体化的友元,并访问了ManyFriend
对象的item成员和ManyFriend
对象的item成员。
如果能为类型指定别名将会很方便,尤其是在模板设计中。可以使用typedef为模板具体化指定别名:
typedef std::array<double, 12> arrd;
typedef std::array<int, 12> arri;
typedef std::array<std::string, 12> arrst;
arrd gallons; //gallons是std::array
arri days; //days是std::array
arrst months; //months是std::array
C++11新增一项功能——使用模板提供一系列别名。如:
template<typename, T>
using arrtype = std::array<T, 12>; //创建多个模板名称
这将arrtype定义为一个模板别名,可使用它来指定类型,比如:
arrtype<double> gallons; //gallons是std::array
arrtype<int> days; //days是std::array
arrtype<std::string> months; //months是std::array
arrtype
表示类型std::array
。
C++11允许将语法using = 用于非模板。用于非模板时这种语法和常规typedef等价:
typedef const char* pc1;
using pc2 = const char*;
typedef const int *(*pa1)[10];
using pc2 = const int *(*)[10];
这种语法可读性更强,因为它让类型名和类型信息更清晰。