C++ 模板的实例化

模板(Template)并不是真正的函数或类,它仅仅是编译器用来生成函数或类的一张 “图纸”。模板不会占用内存,最终生成的函数或者类才会占用内存。由模板生成函数的过程叫做模板的实例化,相应地,针对某个类型生成的特定版本的函数或类叫做模板的一个实例。

在学习模板以前,如果想针对不同的类型使用相同的算法,就必须定义多个及其相似的函数或类,这样不但做了很多重复性的工作,还导致代码维护困难,用于交换两个变量的值的 Swap() 函数就是一个典型代表。而有了模板后,这些工作都可以交给编译器了,编译器会帮助我们自动地生成这些代码。从这个角度理解,模板也可以看做是编译器的一组指令,它命令编译器生成我们想要的代码。

模板的实例化是按需进行的,用到哪个类型就生成针对哪个类型的函数或类,不会提前生成过多的代码。也就是说,编译器会根据传递给类型参数的实参(也可以是编译器自己推演出来的实参)来生成一个特定版本的函数或类,并且相同的类型只生成一次。实例化的过程也很简单,就是将所有的类型参数用实参代替。

例如,给定下面的函数模板和函数调用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
template<typename T> void Swap(T &a, T &b) {
T temp = a;
a = b;
b = temp;
}

int main()
{
int n1 = 100, n2 = 200, n3 = 300, n4 = 400;
float f1 = 12.5, f2 = 56.93;

Swap(n1, n2); // T 为 int,实例化出 void Swap(int &a, int &b);
Swap(f1, f2); // T 为 float,实例化出 void Swap(float &a, float &b);
Swap(n3, n4); // T 为 int,调用刚才生成的 void Swap(int &a, int &b);

return 0;
}

编译器会根据不同的实参实例化出不同版本的 Swap() 函数。对于 Swap(n1, n2) 调用,编译器会生成并编译一个 Swap() 版本,其中 T 被替换为 int:

1
2
3
4
5
void Swap(int &a, int &b) {
int temp = a;
a = b;
b = temp;
}

对于 Swap(f1, f2) 调用,编译器会生成另一个 Swap 版本,其中 T 被替换为 float。对于 Swap(n3, n4) 调用,编译器不会再生成新版本的 Swap() 了,因为刚才已经针对 int 生成了一个版本,直接拿来使用即可。

另外需要注意的是类模板的实例化,通过类模板创建对象时并不会实例化所有的成员函数,只有等到真正调用它们时才会被实例化;如果一个成员函数永远不会被调用,那它就永远不会被实例化。这说明类的实例化是延迟的、局部的,编译器并不着急生成所有的代码。

通过类模板创建对象时,一般只需要实例化成员变量和构造函数。成员变量被实例化后就能知道对象的大小了(占用的字节数),构造函数被实例化后就能够知道如何初始化了;对象的创建过程就是分配一块大小已知的内存,并对这块内存进行初始化。

请看下面的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
#include <iostream>
using namespace std;

template<typename T1, typename 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<typename T1, typename T2>
void Point<T1, T2>::display() const {
cout << "x=" << m_x << ", y=" << m_y << endl;
}

int main()
{
Point<int, int> p1(10, 20);
p1.setX(40);
p1.setY(50);
cout<<"x="<<p1.getX()<<", y=" <<p1.getY()<<endl;

Point<char*, char*> p2("东经180度", "北纬210度");
p2.display();

return 0;
}

输出:

1
2
x=40, y=50
x=东经180度, y=北纬210度

p1 调用了所有的成员函数,整个类会被完整地实例化。p2 只调用了构造函数和 display() 函数,剩下的 get 函数和 set 函数不会被实例化。

值得提醒的是,Point<int, int> 和 Point<char, char> 是两个相互独立的类,它们的类型是不同的,不能相互兼容,也不能自动地转换类型,所以诸如 p1 = p2; 这样的语句是错误的,除非重载了 = 运算符。