一、问题引入
大多数 C/C++ 程序员大致上都按照以下方式来组织他们的 non-template 程序代码:
Classes 和其它类型被全体放置于头文件(header files)。通常头文件的后缀名称(扩展名) 为.hpp(或 .H, .h, .hh, .hxx 等等)。
全局变量和 non-inline 函数只在头文件中置入声明语句, 定义式则置于 .C 文件。 这里的 .C 文件是个统称,通常其后缀名称(扩展名)为.cpp(或.C, .c, .cc, .cxx 等等)。
这种方式运作良好,程序能够轻易找到各个类型的定义,并避免一个变量或函数被多次重复定 义,于是链接器(linker)可以正常工作。
如果牢记上述规则,很多 template 初学者就会犯一个常见错误。让我以下面这个例子加以说明。
就像组织一般 non-template 程序代码一样,我们把 template 声明于某个头文件:
// basics/myfirst.hpp
#ifndef MYFIRST_HPP
#define MYFIRST_HPP
// template 声明语句
template <typename T>
void print_typeof (T const&);
#endif // MYFIRST_HPP
print_typeof()是一个简单的辅助函数,它打印参数的类型信息(type information)。函数
的实际代码被置于一个.C 文件中:
// basics/myfirst.cpp
#include <iostream>
#include <typeinfo>
#include "myfirst.hpp"
// template 的实作码/定义式
template <typename T>
void print_typeof (T const& x) {
std::cout << typeid(x).name() << std::endl;
}
这个例子使用 typeid 运算符,把参数的类型以字符串形式打印出来。
此后,我们又在另外一个.C文件使用这个 template,并将 template 声明语句(的所在文件)以 #include 包含进来:
// basics/myfirstmain.cpp
#include "myfirst.hpp"
// 使用含入之 template
int main() {
double ice = 3.0;
print_typeof(ice); // 以 double 值调用 function template
}
大多数 C++ 编译器都可以正确编译上述程序代码,然而大多数链接器会报告一个错误,表示无法 找到 print_typeof()函数的定义。
错误的原因在于,function template print_typeof()的定义并没有被实例化。为了实例化一个 template,编译器必须知道「以哪一份定义式」以及「以哪些 template arguments」对它实例化。 不幸 的是先前这个例子中, 这两项信息被分置于两个分开编译的文件。 因此当编译器看 到对 print_typeof()的调用时,看不到其定义,无法以 double 类型来实例化 print_typeof()。 于是编译器假设这个 template 的定义位于其它某处,因而只生成一个对该定义的 reference,并将这个reference所指的定义式留给链接器 去决 议( resolve )。另一方 面,当编译 器处理 myfirst.cpp 时,它又察觉不到该以哪个自变量类型来实例化其定义式。
二、解决问题思路
方法一:把 template 定义式放到其声明语句所在的头文件中。面对前例,我们可以在 myfirst.hpp 最后一行加入: #include "myfirst.cpp"
方法二:在用到该template 的每一个 .C文件中 #include "myfirst.cpp"。
方法三:完全丢开 myfirst.cpp,把声明和定义全部放进 myfirst.hpp:
// basics/myfirst2.hpp
#ifndef MYFIRST_HPP
#define MYFIRST_HPP
#include <iostream>
#include <typeinfo>
// template 的声明语句
template <typename T>
void print_typeof (T const&);
// template 的实作码/定义式
template <typename T>
void print_typeof (T const& x) {
std::cout << typeid(x).name() << std::endl;
}
#endif // MYFIRST_HPP
这种 templates 组织法称为「置入式模型」(inclusion model)。现在你会发现,上述程序可被正确编译、链接、执行。
有一些需要注意的地方。最该引起注意的是:「含入 myfirst.hpp」的代价相当大。此处所谓 代价,不仅是指 template 定义式的体积增加,也包括含入 myfirst.hpp 时一并含入的其它(由 myfirst.hpp 含入的)文件,本例是<iostream> 和 <typeinfo>。你会发现,这可能导致成千上万行程序代码被 含 进来, 因为诸如<iostream> 这样的文件也是 以 此种方式来定 义 templates。
这是一个很实际的问题。这显然会在编译大型程序时显著增加编译时间。很多现实世界中的程序会吃掉你数小时的编译+链接时间(关于这个我 们有多次经验,编译和链接一个程序,竟用掉悠悠数天工夫)。
尽管存在这个问题,我们仍然强烈推荐:只要可能,你应该使用置入式模型(inclusion model) 来组织你的 template 程序代码。虽然稍后还将审查另两种办法,但那些办法所带来工程缺陷看起 来远比时间占用问题更严重得多(不过它们也可能带来软件工程之外的其它好处)。