【C++】模板为什么不支持分离编译?

为什么代码需要分离编译?

分离编译的原理

从编译的角度来说,声明文件.h在编译的时候不会为其分配空间,而在编译定义文件,即.c/.cpp文件时,编译器为每个函数,全局变量分配内存空间

在程序的预编译、编译和汇编阶段,如果程序中调用了某个函数,需要这个函数的声明或者实现,如果仅有函数的声明而没有实现,编译器就会在目标文件中做一个标记,告诉链接器在链接的时候需要去找这个函数的定义,把问题移交给链接器,如果链接器找不到,就会报错:undefined reference

从这个原理出发,一个.h文件可以被多个文件包含,前提是这个.h文件没有定义行为,如果有类似于

int val=0void func(){}

这种定义行为,当这个.h文件在被两个及以上文件包含时,在编译时就会出现val或者func()被两个文件同时定义,链接器在对目标文件进行链接的时候就会发现同时有两个val或func(),就是重复定义行为,而声明不会分配内存空间,即不会存在于目标文件中,多个声明并不会影响编译过程。

同时,将声明和定义分离编译有利于程序的移植和复用

C++模板的分离编译产生的问题

定义三个文件:test.cpp、test.h、main.cpp

test.h

template<class T1,class T2>
void func(T1 t1,T2 t2);

test.cpp

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

using namespace std;

template<class T1,class T2>
void func(T1 t1,T2 t2){
    cout<<t1<<t2<<endl;
}

main.cpp

#include "test.h"
#include <string>

int main(){
    string str("downey");
    int i=5;
    func(str,5);
}

标准的分离编译模式:在test.h中声明func(),在test.cpp中定义func()实现,在main.cpp中包含test.h头文件并调用func()

键入编译命令:

g++ test.cpp test.h main.cpp -o test

结果却是:

/tmp/ccqLWRwf.o: In function `main`:
main.cpp:(.text+0x6c): undefined reference to `void func<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, int>(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, int)'
collect2: error: ld returned 1 exit status  

我在test.cpp中已经定义了func()函数,为什么还会报错呢?

模板的编译模式

让我们从编译器的角度出发来观察模板编译的问题,做一些假设:

假设1:如果我只用test.cpp和test.h文件来编译出.o文件会发生什么?

首先,这种方式是可行的,用g++提供的-C参数对目标文件只编译不链接:

g++ -C test.cpp test.h -o test.o

生成的test.o文件,linux下查看目标文件符号表:

shen@ubuntu-vm:~/code/test$ objdump -t test.o

test.o:     file format elf64-x86-64

SYMBOL TABLE:
0000000000000318 l    d  .interp        0000000000000000              .interp
0000000000000338 l    d  .note.gnu.property     0000000000000000              .note.gnu.property
...
0000000000000000       F *UND*  0000000000000000              __libc_start_main@@GLIBC_2.2.5
0000000000000000  w      *UND*  0000000000000000              __gmon_start__
0000000000000000  w      *UND*  0000000000000000              _ITM_registerTMCloneTable
0000000000000000       F *UND*  0000000000000000              _ZNSt8ios_base4InitD1Ev@@GLIBCXX_3.4

结果找不到func符号的影子,也就是说编译出的二进制文件中根本没有为func()分配内存空间,这是为什么呢?

答案是:模板只有在使用的时候才进行实例化,才会生成符号

通俗地说,用户写一个模板定义,模板本身提供任意数据类型的支持,而编译器在编译出的目标文件中只支持确定的类型例如func(int,int)、func(string char),而不能支持func(模板类,模板类…)(注意模板是C++的特性而非编译器的特性)

那么编译器在编译的时候根本不知道用户要传入什么样的参数,所以无法确定模板的实例,所以编译器只能等到用户使用此模板的时候才能进行实例化,才能确定模板的具体类型,从而为其分配内存空间,生成符号,像上述例子中,模板没有使用,也就不会实例化,编译器也就不会为模板分配内存空间。

那为什么在同时编译test.cpp、test.h、main.cpp的例子中,模板在main函数中有进行实例化,还是会提示无定义行为呢?

事实上编译器是对所有的.cpp文件分开编译的:

main.cpp包含test.h,就会将main.cpp和test.h编译成main.o目标文件

test.cpp包含test.h,将test.cpp和test.h编译成test.o目标文件

然后链接器将main.o和test.o以及一些标准库链接成可执行文件。

但是由以上的分析得知,在将test.cpp和test.h编译成test.o的过程中编译器并没有将func()实例化,也就是没有func()的定义,不会产生符号,然而main.o中使用了func函数,需要在其他的.o或者.so中寻找符号定义的地方,所以在链接的时候出现未定义行为

我们现在知道了,为什么模板方法声明和定义一定要放到一起。因为每个cpp文件单独编译,编译test.cpp时没有调用,所以不会生成符号,而编译main.cpp时,只能找到声明的地方,需要等到链接的时候在其他.o或者.so库中找到定义的地方,而此时test.o中又没有定义的地方,就会出现链接错误

那如果我在.cpp文件中调用模板方法呢?

即test.cpp变成:

#include "test.h"

template<class T1,class T2>
void func(T1 t1,T2 t2){
    cout<<t1<<t2<<endl;
}

void function(string str,int val){
    func(str,val);	   //将func实例化
}

main.cpp保持不变:

#include "test.h"

int main(){
    string str("downey");
    int i=5;
    func(str,5);
}

这时候编译并查看符号表:

在这里插入图片描述
发现在ELF文件中已经出现了符号,即模板函数func已经实例化了

编译器是否能完全地支持这个模板函数呢?

在上述例子中需要注意到的是,在模板定义文件中使用的是func(string, int),在main.cpp中使用的也是func(string,int),那在main.cpp中是否也能支持模板的其他重载函数呢?例如func(string, char)或者func(string,string)?

对此,需要修改main函数为:

#include "test.h"
#include <string>

using namespace std;

int main(){
    string str("downey");
    func(str,str);
}

结果是:

在这里插入图片描述

链接错误,说明在main.cpp中调用的函数为func(string,string),而我们在test.cpp中实例化的是func(string,int),所以会出现链接错误

如果需要用到func(string,string),就得在test.cpp文件中将func(string,string)实例化一次,也就是调用一次

解决方案1:提前实例化模板

看到这里,各位观众们应该大概能想到一种分离编译的解决方案了,在模板定义文件中将所有要用到的模板函数都进行实例化,不过说实话,这很扯蛋,而且完全不符合程序的美学。

设计泛型接口本身就是为了多态,并不需要知道调用者以什么方式调用,这样实现的话接口设计者就得知道调用者的所有调用方式!但事实上是确实可以这么做。

解决方案2:定义实现全部放在同一个.h文件中

第二种解决方案就是将模板的定义和实现全部放到一个.h文件中,为什么这样又可以呢?

当某个.cpp文件用到某个模板时,包含了相应的头文件,而头文件中同时由模板的定m义和声明,在cpp文件中使用就相当于对这个模板进行了实例化(.cpp文件依赖.h文件编译成.o文件,cpp文件中实例化,h文件中进行定义和声明),这样就可以使用模板了。

这也是常见的做法,STL就是这样实现的,遗憾的是,这违背了分离编译的思想。

C++模板不支持分离编译的思考

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值