Chapter16:模板(二)——显式具体化和显式实例化
一、改变模板的“游戏规则”
C++ 没有办法限制类型参数的范围,我们可以使用任意一种类型来实例化模板。但是模板中的语句(函数体或者类体)不一定就能适应所有的类型,可能会有个别的类型没有意义,或者会导致语法错误。
比如说在模板函数内比较值的大小,这对一些基本数据类型有用,但是却不能用来比较结构体、类和数组等数据——我们并没有针对它们进行重载。对于指针来说,比较的是地址大小,而不是指针指向的数据,所以也没有现实意义。总之,我们必须对它们进行单独处理。
模板是一种泛型技术,它能接受的类型是宽泛的、没有限制的,并且对这些类型使用的算法都是一样的(函数体或类体一样)。但是现在我们希望改变这种“游戏规则”,让模板能够针对某种具体的类型使用不同的算法(函数体或类体不同),这在 C++ 中是可以做到的,这种技术称为模板的 显式具体化 (Explicit Specialization)。
模板函数如何显式实例化?看一下这个例子:
#include <iostream>
#include <string>
using namespace std;
typedef struct{
string name;
float score;
} STU;
template<typename T> const T & Max(const T &a, const T &b){
return a > b ? a : b;
template<> const STU & Max<STU>(const STU &a, const STU &b){
return a.score > b.score ? a : b;
ostream & operator << (ostream & out, const STU &stu){
out << stu.name << ' ' << stu.score;
return out;
int main(int argc, char const *argv[]){
int a = 10, b = 20;
cout<<Max(a, b)<<endl;
STU stu1 = {"Sam", 90}, stu2 = {"Amy", 100};
cout<<Max(stu1, stu2);
return 0;
请格外注意这一行:
template<> const STU & Max<STU>(const STU &a, const STU &b)
Max<STU>中的
STU
表明了要将类型参数 T 具体化为 STU 类型,原来使用 T 的位置都应该使用 STU 替换,包括返回值类型、形参类型、局部变量的类型。Max 只有一个类型参数 T,并且已经被具体化为 STU 了,这样整个模板就不再有类型参数了,类型参数列表也就为空了,所以模板头应该写作template<>。另外,Max<STU>中的
STU
是可选的,因为函数的形参已经表明,这是 STU 类型的一个具体化,编译器能够逆推出 T 的具体类型。简写方式如下所示:
template<> const STU& Max(const STU& a, const STU& b);
在 C++ 中,对于给定的函数名,可以有非模板函数、模板函数、显示具体化模板函数以及它们的重载版本,在调用函数时,显示具体化优先于常规模板,而非模板函数优先于显示具体化和常规模板。
那么,类模板如何显式具体化呢?请看下面这个例子:
#include <iostream>
using namespace std;
template<class T1, class T2> class Point{
public:
Point(T1 x, T2 y): m_x(x), m_y(y){ }
public:
T1 getX() const{ return m_x; }
void setX(T1 x){ m_x = x; }
T2 getY() const{ return m_y; }
void setY(T2 y){ m_y = y; }
void display() const;
private:
T1 m_x;
T2 m_y;
template<class T1, class T2>
void Point<T1, T2>::display() const{
cout<<"x="<<m_x<<", y="<<m_y<<endl;
template<> class Point<char*, char*>{
public:
Point(char *x, char *y): m_x(x), m_y(y){ }
public:
char *getX() const{ return m_x; }
void setX(char *x){ m_x = x; }
char *getY() const{ return m_y; }
void setY(char *y){ m_y = y; }
void display() const;
private:
char *m_x;
char *m_y;
void Point<char*, char*>::display() const{
cout<<"x="<<m_x<<" | y="<<m_y<<endl;
int main(){
( new Point<int, int>(10, 20) ) -> display();
( new Point<int, char*>(10, "E180") ) -> display();
( new Point<char*, char*>("E180", "N210") ) -> display();
return 0;
以上方法也称模板类的全特化,一个类被称为全特化类的条件:1.必须有一个主模板类 2.模板类型被全部明确化。也许你会问,既然有非类型参数,那么在显示具体化里面有没有类似的、只为一部分参数提供实参的方法?当然有,模板类型中,可以有被明确化的部分和没有被明确化的部分,这称为偏特化(编译器的设计者怎么会不考虑到如此灵活的设计呢)。可以把上面这个例子改一改:
#include <iostream>
using namespace std;
template<class T1, class T2> class Point{
public:
Point(T1 x, T2 y): m_x(x), m_y(y){ }
public:
T1 getX() const{ return m_x; }
void setX(T1 x){ m_x = x; }
T2 getY() const{ return m_y; }
void setY(T2 y){ m_y = y; }
void display() const;
private:
T1 m_x;
T2 m_y;
template<class T1, class T2>
void Point<T1, T2>::display() const{
cout<<"x="<<m_x<<", y="<<m_y<<endl;
template<typename T2>
class Point<char*, T2>{
public:
Point(char *x, T2 y): m_x(x), m_y(y){ }
public:
char *getX() const{ return m_x; }
void setX(char *x){ m_x = x; }
T2 getY() const{ return m_y; }
void setY(T2 y){ m_y = y; }
void display() const;
private:
char *m_x;
T2 m_y;
template<typename T2>
void Point<char*, T2>::display() const{
cout<<"x="<<m_x<<" | y="<<m_y<<endl;
int main(){
( new Point<int, int>(10, 20) ) -> display();
( new Point<int, char*>(10, "E180") ) -> display();
( new Point<char*, char*>("E180", "N210") ) -> display();
return 0;
对主版本模板类、全特化类、偏特化类的调用优先级从高到低进行排序是:全特化类>偏特化类>主版本模板类。这样的优先级顺序对性能也是最好的。切记一定实现主模板,不然编译器认不出特化的是哪个参数、有没有写全。
写到这里我才发现,前一节写的非具体参数原来就是模板函数的偏特化,考虑不周不好意思TAT。。。,包括偏特化全特化这些特有名词也是半路才发现的。。。
二、模板的实例化与具体实例化
模板(Templet)并不是真正的函数或类,它仅仅是编译器用来生成函数或类的一张“图纸”。模板不会占用内存,最终生成的函数或者类才会占用内存。由模板生成函数或类的过程叫做模板的实例化(Instantiate),相应地,针对某个类型生成的特定版本的函数或类叫做模板的一个实例(Instantiation)。
模板的实例化是按需进行的,用到哪个类型就生成针对哪个类型的函数或类,不会提前生成过多的代码。也就是说,编译器会根据传递给类型参数的实参(也可以是编译器自己推演出来的实参)来生成一个特定版本的函数或类,并且相同的类型只生成一次。实例化的过程也很简单,就是将所有的类型参数用实参代替。
另外需要注意的是类模板的实例化,通过类模板创建对象时并不会实例化所有的成员函数,只有等到真正调用它们时才会被实例化;如果一个成员函数永远不会被调用,那它就永远不会被实例化。这说明类的实例化是延迟的、局部的,编译器并不着急生成所有的代码。
通过类模板创建对象时,一般只需要实例化成员变量和构造函数。成员变量被实例化后就能够知道对象的大小了(占用的字节数),构造函数被实例化后就能够知道如何初始化了;对象的创建过程就是分配一块大小已知的内存,并对这块内存进行初始化。
额外说一下,模板的工作是帮助编译器生成参数数据类型不同的代码,从这个角度理解模板可以是编译器的一组指令,帮我们生成目标代码吧hhh
通常模板的实例化是在调用函数或者创建对象时由编译器自动完成的,不需要程序员引导,因此称为隐式实例化。相对应的,我们也可以通过代码明确地告诉编译器需要针对哪个类型进行实例化,这称为显式实例化。
编译器在实例化的过程中需要知道模板的所有细节:对于函数模板,也就是函数定义;对于类模板,需要同时知道类声明和类定义。我们必须将显式实例化的代码放在包含了模板定义的源文件中,而不是仅仅包含了模板声明的头文件中。这样一来,就可以把模板的声明和定义放在不同文件里面了。
举个例子,来看看函数模板的显示实例化吧:
#define MAXNAME 128
struct job
char name[MAXNAME]:
int salary;
template<class T>
void swap(T &a, T &b )
T temp;
temp = a;
a = b;
b = temp;
template void swap<int>(int &a, int & b);
template<> void swap<job>(job &a, job &b)
int salary:
salary = a.salary:
a.salary = b.salary;
b.salary = salary;
再写一个例子,看一下类的显式实例化:
#include <iostream>
using namespace std;
const int MAXSIZE = 1000;
template<class T>
class Seqlist
public:
Seqlist() { length = 0; }
Seqlist(const T a[], int n);
int locate(T x);
T get(int i);
private:
T data[MAXSIZE];
int length;
template<class T>
Seqlist<T>::Seqlist(const T a[], int n)
if (n > MAXSIZE)throw"数组长度超过最大长度";
for (int i = 0; i < n; i++)
data[i] = a[i];
length = n;
template<class T>
T Seqlist<T>::get(int i)
if (i<1 || i>length)throw"位置非法";
return data[i - 1];
template<class T>
int Seqlist<T>::locate(const T x)
for (int i = 0; i < length; i++)
if (x == data[i])
return i + 1;
return 0;
int main()
int a[7] = { 1,2,3,4,5,6,7 };
Seqlist<int>list(a, 7);