为什么C++模板不支持分离式编译

前言

最近编译C++代码时出现链接失败信息,类似下图:

图一

初见这个错误有些令人费解,不过经过一番分析,发现原因还是清晰的,和大家一起分享一下。

图一中使用的tpl.h代码如下:

#pragma once

template <typename T>
int compare(const T& a, const T& b);

tpl.cpp代码如下:

#include "tpl.h"
#include <iostream>

template <typename T>
int compare(const T& a, const T& b) {
    if (a == b) {
        return 0;
    }
    return (a > b) ? 1 : -1;
}

main.cpp代码如下:

#include <iostream>
#include "tpl.h"

int main() {
    int res = compare<int> (1, 2);
    std::cout << res << std::endl;
}

我一直习惯把模板实现写在头文件中,因此从未遇见这种错误。这次偶然将模板声明和实现分离,出现了链接错误。这引发了我一些思考,为什么模板不支持分离式编译?

模板的编译

为了搞清楚模板是怎么编译的,这里以上述tpl.cpp中的compare模板函数为例。tpl.h中声明了一个模板函数并且在tpl.cpp中实现这个模板函数。现在我们编译tpl.cpp,生成的汇编代码如下:

图二

可以看出,一个模板如果没有被调用的时候,编译器不会对这部分代码做任何处理,一行指令都没有。实际上编译器也不知道要怎么处理这个模板。在没有调用之前,模板的参数类型是不确定的。

假如我们在tpl.cpp中增加调用compare的函数的test函数,如下:

int test() {
    int res = compare<int> (1, 2);
    std::cout << res << std::endl;
    return res;
}

再编译tpl.cpp就会发现,生成的汇编文件里面已经有了以int为参数的compare函数了,如下图:

图三

链接错误问题分析

那么图一中的链接错误是怎么发生的?我们执行了g++ tpl.cpp main.cpp这个命令,大致会经过下面这三个步骤:

  1. 编译并且汇编tpl.cpp,生成tpl.o目标文件。
  2. 编译并且汇编main.cpp,生成main.o目标文件。
  3. 链接tpl.o和main.o生成可执行文件。

编译tpl.cpp已经分析过了,由于没有调用模板函数,因此编译器不会对模板做任何处理。实际上编译器根本不知道要怎么处理,因为模板里面的类型是不确定的。只有在调用的时候才会确定下来,这个时候编译器才知道这个函数长什么样,才会执行编译操作。

编译main.cpp的时候,由于模板compare已经声明但未实现,因此这里也不会生成具体代码,只会生成一个call指令。很显然这个call指令中的函数地址现在肯定是错误的,头文件tpl.h并没有去实现这个函数,需要依赖链接器将这个地址修改成正确的地址。

问题就出在编译tpl.cpp的时候模板没有实例化,编译器并没有编译这个函数,因此找在符号表中找不到这个函数的地址,链接器不知道要怎么处理,因此会出现链接错误。

如何改正

比较好的方式是在头文件中定义模板而不仅仅只是声明。

比如上面的tpl.h代码改成这样

#pragma once

template <typename T>
int compare(const T& a, const T& b) {
    if (a == b) {
        return 0;
    }
    return (a > b) ? 1 : -1;
}

就不会有链接问题了。

赞赏

微信赞赏支付宝赞赏

其他

发表评论

您的电子邮箱地址不会被公开。 必填项已用*标注