0%

C++ 模板的显式实例化

前面讲到的模板的实例化是在调用函数或者创建对象时由编译器自动完成的,不需要程序员引导,因此称为 隐式实例化。相对应的,我们也可以通过代码明确地告诉编译器需要针对哪个类型进行实例化,这称为 显式实例化

编译器在实例化的过程中需要知道模板的所有细节:对于函数模板,也就是函数定义;对于类模板,需要同时知道类声明和类定义。我们必须将显式实例化的代码放在包含了模板定义的源文件中,而不是仅仅包含了模板声明的文件中。

函数模板的显式实例化

以上节讲到的 Swap() 函数为例,针对 double 类型的显式实例化代码为:

1
template void Swap(double &a, double &b);

这行代码由两部分组成,前边是一个 template 关键字(后面不带 <>),后面是一个普通的函数原型,组合在一起的意思是:将模板实例化成和函数原型对应的一个具体版本。

将该代码放到 func.cpp 文件的最后,再运行程序就不会再出错了。

另外,还可以在包含了函数调用的源文件(main.cpp)中再增加下面的一条语句:

1
extern template void Swap(double &a, double &b);

该语句在前面增加了 extern 关键字,它的作用是明确地告诉编译器,该版本的函数实例在其他文件中,请在链接期间查找。不过这条语句是多余的,即使不写,编译器发现当前文件中没有对应的模板定义,也会自动去其他文件中查找。

上节我们展示了一个反面教材,告诉大家不能把函数模板的声明和定义分散到不同的文件中,但是现在有了显式实例化,这一点就可以做到了,下面就上节的代码进行修复。

func.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//交换两个数的值
template<typename T> void Swap(T &a, T &b){
T temp = a;
a = b;
b = temp;
}

//冒泡排序算法
void bubble_sort(int arr[], int n){
for(int i=0; i<n-1; i++){
bool isSorted = true;
for(int j=0; j<n-1-i; j++){
if(arr[j] > arr[j+1]){
isSorted = false;
Swap(arr[j], arr[j+1]); //调用Swap()函数
}
}
if(isSorted) break;
}
}

template void Swap(double &a, double &b); // 显式实例化定义

func.h

1
2
3
4
5
6
7
#ifndef _FUNC_H
#define _FUNC_H

template<typename T> void Swap(T &a, T &b);
void bubble_sort(int arr[], int n);

#endif

main.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <iostream>
#include "func.h"
using namespace std;

//显示实例化声明(也可以不写)
extern template void Swap(double &a, double &b);
extern template void Swap(int &a, int &b);

int main(){
int n1 = 10, n2 = 20;
Swap(n1, n2);

double f1 = 23.8, f2 = 92.6;
Swap(f1, f2);
return 0;
}

显式实例化也包括声明和定义,定义要放在模板定义(实现)所在的源文件,声明要放在模板声明所在的头文件(当然也可以不写)。

类模板的显式实例化

类模板的显式实例化和函数模板类似。以上节的 Point 类为例,针对 char* 类型的显式实例化(定义形式)代码为:

1
template class Point<char*, char*>;

相应地,它的声明形式为:

1
extern template class Point<char*, char*>;

不管是声明还是定义,都要带上 class 关键字,以表明这是针对类模板的。

另外需要注意的是,显式实例化一个类模板时,会一次性实例化该类的所有成员,包括成员变量和成员函数。

有了类模板的显式实例化,就可以将类模板的声明和定义分散到不同的文件中了,下面我们就来修复上节的代码。

point.cpp

1
2
3
4
5
6
7
8
9
10
11
12
#include<iostream>
#include "point.h"
using namespace std;

template<class T1, class T2>
void Point<T1, T2>::display() const {
cout << "x=" << m_x << ", y=" << m_y << endl;
}

// 显式实例化定义
template class Point<char*, char*>;
template class Point<int, int>;

point.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#ifndef _POINT_H
#define _POINT_H

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;
};

#endif

main.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <iostream>
#include "point.h"
using namespace std;

//显式实例化声明(也可以不写)
extern template class Point<char*, char*>;
extern template class Point<int, int>;

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
extern template declaration; // 实例化声明
template declaration; // 实例化定义

对于函数模板来说,declaration 就是一个函数原型;对于类模板来说,declaration 就是一个类声明。

显式实例化的缺陷

C++ 支持显式实例化的目的是为模块化编程提供一种解决方案,这种方案虽然有效,但是也有明显的缺陷:程序员必须要在模板的定义文件(实现文件)中对所有使用到的类型进行实例化。这就意味着,每次更改了模板使用文件(调用函数模板的文件,或者通过类模板创建对象的文件),也要相应地更改模板定义文件,以增加对新类型的实例化,或者删除无用类型的实例化。

一个模板可能会在多个文件中使用到,要保持这些文件的同步更新是非常困难的。而对于库的开发者来说,他不能提前假设用户会使用哪些类型,所以根本就无法使用显式实例化,只能将模板的声明和定义(实现)全部放到头文件中;C++ 标准库几乎都是用模板来实现的,这些模板的代码也都位于头文件中。

总的来说,如果我们开发的模板只有我们自己使用,那也可以勉强使用显式实例化;如果希望让其他人使用(例如库、组件等),那只能将模板的声明和定义都放到头文件了。