模板的链接

在链接过程中,模板让事情变得更复杂。

首先了解最基本的一个事实:单一定义原则在考察整个程序时,只关心符号的定义数量。对于类、枚举和模板,允许在整个程序的不同翻译单元中多次出现定义。当然,这同样要求每个定义是完全一致的,否则导致未定义行为。

因此直接将模板定义放到头文件里是没有问题的。即便它被不同的翻译单元包含,最终链接的时候也不会给出重复定义的错误。

// mylib.h
template<typename T>
void f(T t) {
    // do something...
}

那么可不可以把模板的定义与声明分开呢?就像这样,最后链接的时候再给出 mylib.cpp 中的模板定义:

// mylib.h
template<typename T>
void f(T t);

// mylib.cpp
template<typename T>
void f(T t) {
    // do something...
}

答案是否定的。这种写法会导致链接错误。这是因为模板的实例化机制导致的。为了讨论方便,假设 a.cpp 使用了 f 模板:

// a.cpp
#include "mylib.h"
int main() {
    f(42); // 期望一个 f<int> 实例化
}

这里使用了 f(42);,于是类型实参推导为 int,即需要一个 f<int> 的实例化结果。但是,mylib.h 并没有给出 f 的定义,所以需要在链接的时候去找 f<int> 的定义。

然后 mylib.cpp 给出了模板 f 的定义。但是,mylib.cpp 并没有以任何方式使用 f 模板,所以不会进行 f 的任何实例化。最后的结果就是,mylib.cpp 单独编译后不会存在一个叫做 f<int> 的定义。

两者链接的时候,a.o 缺失了 f<int> 定义,但 mylib.o 也没有这个定义。然后,链接失败,给出未定义错误:

ld: a.o:a.cpp:(.text+0x13): undefined reference to `void f<int>(int)'

那有没有什么应急的办法呢?问题出在了 mylib.cpp 不会实例化 f<int>,那么就需要强行让它实例化这样一个函数出来。我们过去通过使用函数调用表达式的方式来实例化一个模板,称为“隐式实例化”,而下面这种明明白白地告诉编译器要实例化的语法,称为“显式实例化”:

// mylib.cpp
template<typename T>
void f(T t) {
    // do something...
}
// 显式实例化 f<int>
template void f<int>(int t);
// (实参列表 <int> 可省略,从形参推导)

这种语法

template 返回值类型 函数模板名<模板实参列表>(函数形参列表);
template classstruct 类模板名<模板实参列表>;

称为显式实例化定义:它既不是模板声明也不是函数或类声明,就单纯是一种不引入名字的、迫使编译器生成一份实例化结果的语法。当有了这样一个显式实例化定义后,刚才的代码也就能够链接通过了。

但这种语法用在库里面是不合适的:你不知道用户会用哪些模板实参来实例化。如果用了 main.cpp 内自定义的一个类型,那就一点办法也没有了。所以,设计模板时,总是建议将定义直接放在头文件

现在来考察成员函数。不管是类模板的成员函数,还是类的成员函数模板,亦或是类模板的成员函数模板,它们都需要实例化。一旦需要实例化,就没法把定义分到单独的翻译单元去。所以,有关模板的成员函数,也请在头文件中写下。至于是类外定义还是类内定义,这是无所谓的,取决于你的编码习惯。

// mylib.h
template<typename T>
struct A {
    // 直接类内定义...
    void f() {
        // do something...
    }
    void g();
};

// ...或者类外定义,但仍然在头文件中
template<typename T>
void A<T>::g() {
    // do something...
}

最后一样是非常容易忽略的:模板类的友元函数。但它实在是太复杂了,以至于我需要新开一节来讲。

最近更新:
代码未运行