作者 青鸟

根据传统的c++编程习惯,我们一般会把一个函数的声明放在.h文件中,把函数的实现放在.cpp文件中,但是在模板编程中,这种分离编译的方式会带来些问题,现在重点讨论一下这些问题及相应的解决方法。

问题

举个例子,当我们将模板类的声明与具体的实现分开的时候,在编译的过程中会抛出错误,抛出的错误是链接错误。
下面给出个示例:

下面给出List.h的代码

1
2
3
4
5
6
7
8
template<typename T>
class List {
private:
Node<T>* head;
public:
void print();
List();
};

下面给出List.cpp的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <iostream>
#include "List.h"

template<typename T>
void List<T>::print() {
for(Node<T>* p= head;p->getNext();p=p->getNext())std::cout<<p->getValue()<<" ";
std::cout<<std::endl;
}

template<typename T>
List<T>::List() {
std::cout<<"请输入数字,以0结尾表示结束"<<std::endl;
Node<T> *p= nullptr;
head=p= new Node<T>;
int x;
while(std::cin>>x){
if(x==0)break;
p->setValue(x);
p->setNext(new Node<T>);
p=p->getNext();
}
p->setNext(nullptr);
}

下面给出main.cpp

1
2
3
4
5
6
7
#include "List.h"
#include "List.cpp"
int main() {
List<int>list;
list.print();
return 0;
}

大部分C++编译器(Compiler)很可能会接受这个程序,没有任何问题,但是链接器(Linker)大概会报告一个错误,指出缺少函数print()的定义。

分析

这个错误的原因在于,模板函数print()的定义还没有被具现化(instantiate)。为了具现化一个模板,编译器必须知道哪一个定义应该被具现化,以及使用什么样的模板参数来具现化。不幸的是,在前面的例子中,这两组信息存在于分开编译的不同文件中。因此,当我们的编译器看到对print()的调用,但是没有看到此函数为int类型具现化的定义时,它只是假设这样的定义在别处提供,并且创建一个那个定义的引用(链接器使用此引用解析)。另一方面,当编译器处理List.cpp时,该文件并没有任何指示表明它必须为它所包含的特殊参数具现化模板定义。

在使用模板类时最常犯的错误是将模板类视为某种数据类型。所谓类型参量化(parameterized types)这样的术语导致了这种误解。模板当然不是数据类型,模板就是模板,恰如其名:

  1. 编译器使用模板,通过更换模板参数来创建数据类型。这个过程就是模板实例化
  2. 从模板类创建得到的类型称之为特例(specialization)。
  3. 模板实例化取决于编译器能够找到可用代码来创建特例(称之为实例化要素,point of instantiation)。
  4. 要创建特例,编译器不但要看到模板的声明,还要看到模板的定义。

再回头看上面的例子,可以知道List是一个模板,List是一个模板实例中的一个int类型。从List创建List的过程就是实例化过程。实例化要素体现在main.cpp文件中。如果按照传统方式,编译器在List.h文件中看到了模板的声明,但没有模板的定义,这样编译器就不能创建类型List。但这时并不出错,因为编译器认为模板定义在其它文件中,就把问题留给链接程序处理。

那么编译List.cpp时会发生什么问题呢?编译器可以解析模板定义并检查语法,但不能生成成员函数的代码。它无法生成代码,因为要生成代码,需要知道模板参数,即需要一个类型,而不是模板本身。

当没有给定模板的具体参数时,编译器是无法生成对应的二进制代码,c++的标准表明当一个模板用不到时不应该呗具象出来

关键是:在分离式编译的环境下,编译器编译某一个.cpp文件时并不知道另一个.cpp文件的存在,也不会去查找。 当遇到未决符号时它会寄希望于连接器。这种模式在没有模板的情况下运行良好,但遇到模板时就会出现错误,因为模板仅在需要的时候才会具现化出来,所以,当编译器只看到模板的声明时,它不能具现化该模板,只能创建一个具有外部连接的符号并期待连接器能够将符号的地址决议出来。然而当实现该模板的.cpp文件中没有用到模板的具现体时,编译器懒得去具现,所以,整个工程的中就找不到模板具现的相应的二进制代码。

解决

第一种方法是将模板和模板特例化一起放在对应的.h文件中。简单来说就是把.cpp和.h合并写成一个文件,使它包含所有的模板声明与定义

第二种解决方式是采用与我们使用宏或者内联函数相同的方法:我们将模板的定义包含进声明模板的头文件中。对于我们的例子,我们可以通过将#include "List.cpp"添加到List.h文件尾部,或者在每一个使用我们的模板的点C文件中包含List.cpp文件,来达到目的,这也是比较推荐的解决办法

这种组织模板代码的方式就称作包含模式。经过这样的调整,你会发现我们的程序已经能够正确编译、链接、执行了。

参考文献:

C++中模板以及模板实例化都放在头文件

《C++ Template: The Complete Guide》