为什么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;
}

就不会有链接问题了。

你可能还喜欢下面这些文章

GDB入门:使用bt查看程序出core的调用栈

当程序崩溃的时候,会产生一个core文件。我们可以称它为进程死亡现场。排查进程死亡就和破案一样,找到案发现场,仔细排查每个细节,抽丝剥茧,最终定位原因。很幸运我们有一个强大的工具调查现场信息。这个工具就是GDB。下面我们就来看看如何用GDB排查问题。首先以一个越界访问数组的程序为例,如下:#include #include void core() { std::vector<int> a; std::cout << a[0];}int main() { core(); return 0;}执行上面的代码将会产生一个core文件。假设我们的core

Shell中的条件判断语句if的用法

shell中的if语法是最让我头疼的语法之一,它的判断就向使用USB插头一样——拿起来插入不行,翻转再插入还不行,再翻转插入行了!为了搞清楚这部分语言,我收集了一些文章关于if条件判断的用法,希望对你也有些帮助。一、基本语法if [ command ]; then 符合该条件执行的语句fiif [ command ];then 符合该条件执行的语句elif [ command ];then 符合该条件执行的语句else 符合该条件执行的语句fi语法说明bash shell会按顺序执行if语句,如果command执行后且它的返回状态是0,则会执行符合该条件执行的语

std::endl为什么导致程序变慢

最近在写hadoop的streaming任务,在输出的时候用了std::endl,就像下面这样:os << “content” << std::endl运行后发现程序跑的比python还慢,令人费解。我入门C++的时候,输出hello world也是这样写的,有什么问题?于是查了一下std::endl,发现问题挺大。std::endl解释如下:Inserts a new-line character and flushes the stream.Its behavior is equivalent to calling os.put(‘\n’) (or os.put(o

c语言的位操作

一、基本位操作|或&与~取反^异或<<左移>>右移二、位操作的常见用法1.获取某位的值#define BitGet(Number,pos) ((Number)|= 1<<(pos)) //把某位置1#define BitGet(Number,pos) ((Number) &= ~(1<<(pos)) //把某位置0#define BitGet(Number,pos) ((Number) >> (pos)&1)) //用宏得到某数的某位#define BitGet(Number,pos) ((Number) ^=

赞赏

微信赞赏支付宝赞赏

发表回复

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