在一般的 C++ 开发中,习惯将函数声明与实现放在不同的文件中,如声明放在 .h 文件,实现放在 .cpp 文件,并在其它地方引用时只包含 .h 文件。但对于 C++ 的模板,这是一个例外,它只能被写在一个文件中。

普通函数

// test.h
int sumInt(int a, int b);
// test.cpp
int sumInt(int a, int b) {
  return a + b;
}

模板函数

// test.cpp
template<typename T>
T sum(T a, T b) {
  return a + b;
}

编译阶段

要理解为什么这样,需要先了解 C++ 的编译、链接过程。首先要知道的是,每个 .cpp 文件会被独立编译为对应的 .obj 文件,这个文件是 .cpp 文件的二进制汇编版本。

但随着模块化设计的发展,可能会出现一个文件调用另一个文件函数的情况,由于独立编译的缘故,这些函数的地址不能被确定,因此生成的 CALL 指令跟随的是一个虚拟函数地址。

链接阶段

当整个编译过程结束后,开始链接流程。即将所有的 .obj 文件链接为对应操作系统的可执行文件,如 Windows 的 .exe 文件。在这个过程中,真实的函数地址才得以确定。

简单来说,编译期间不会检查函数是否实现,它只需要函数的声明。但在链接时,要处理函数间的调用关系,此时才会检查函数的具体实现。

模板的困境

由于 C++ 的模板原理是根据调用方参数类型,生成多个不同的实现。如

sum(1, 2);
sum(1.0, 2.0);

将生成如下两种实现

int sum(int a, int b) { return a + b; }
double sum(double a, double b) { return a + b; }

生成操作在编译阶段完成,若模板函数的声明与实现分离,且在其它地方使用时只引用了 .h 文件,则会由于无法找到对应的模板代码,无法生成出不同的实现。

由于编译阶段只检查函数有无声明,因此不会产生编译错误。但在链接时,会因无法找到与之匹配的实现而报错。

要怎么做

  • 将模板函数的声明与实现写在同一个文件里,按照约定可以命名为 .hpp 文件;
  • (不推荐)若写到了不同文件中,则在使用时将全部相关文件一并引入;