为什么C++编译器不能支持对模板的分离式编译

转载自http://blog.sina.com.cn/s/blog_46625a5f010000ld.html

(可以使用export关键字实现模板的分离编译,但是不是所有的编译器都支持)
为什么C++编译器不能支持对模板的分离式编译,现在下面是 刘未鹏(pongba) 的文章,从网上抄录下来,不知道出处。但是我已经对它进行了整理。

====C++ 编译器的工作简介=========================

在 C++ 标准中提到,一个编译单元 [ Translation Unit ] 是指一个.cpp文件以及它所include的所有.h文件。.h文件里的代码将会被扩展到包含它的.cpp文件里,然后编译器编译该.cpp 文件为一个.obj文件,后者拥有PE [ Portable Executable,即 Windows 可执行文件 ] 文件格式,并且本身包含的就已经是二进制码。但是,不一定能够执行,因为并不保证其中一定有main 函数。当编译器将一个工程里的所有.cpp文件以分离的方式编译完毕后,再由连接器 [ linker ] 进行连接成为一个.exe文件。

举个例子:
//---------------test.h-------------------//
void f();//这里声明一个函数f

//---------------test.cpp--------------//
#include "test.h"
void f()
{
…//do something
} //这里实现出test.h中声明的f函数

//---------------main.cpp--------------//
#include "test.h"
int main()
{
f(); //调用f,f具有外部连接类型
}

在这个例子中,test.cpp 和 main.cpp 各被编译成为不同的 .obj 文件 [ 姑且命名为 test.obj 和 main.obj ] ,在 main.cpp 中,调用了 f 函数。


当编译器编译 main.cpp 时,它所仅仅知道的只是main.cpp中所包含的test.h文件中的一个关于void f();的声明,所以,编译器将这里的 f() 看作外部连接类型,即认为它的函数实现代码在另一个.obj文件中,本例也就是test.obj。

因此 main.obj 中没有任何关于 f() 的代码。这些代码实际存在于test.cpp所编译成的test.obj中。


====C++对方法的调用,编译是如何链接的=================

在main.obj中对f的调用只会生成一行call指令,像这样:
call f [ C++中这个名字当然是经过 mangling 过的 ]

在编译时,这个call指令显然是错误的,因为main.obj中没有f()的实现代码。至于寻找 f() 的实现代码,那就是连接器的任务了。找到以后将call f这个指令的调用地址换成实际的f的函数进入点地址。需要注意的是:连接器实际上将工程里的.obj“连接”成了一个.exe文件,而它最关键的任务就是上面说的,寻找一个外部连接符号在另一个 .obj 中的地址,然后替换原来的“虚假”地址。


这个过程如果说的更深入就是:
call f这行指令其实并不是这样的,它实际上是所谓的stub,也就是一个
jmp 0x23423
[ 这个地址可能是任意的,然而关键是这个地址上有一行指令来进行真正的call f动作。也就是说,这个.obj文件里面所有对f的调用都jmp向同一个地址,在后者那儿才真正”call”f。这样做的好处就是连接器修改地址时只要对后者的call XXX地址作改动就行了。但是,连接器是如何找到f的实际地址的呢[在本例中这处于test.obj中]


因为 .obj 于 .exe 的格式都是一样的,在这样的文件中有一个符号导入表和符号导出表[ import table & export table ] 其中将所有符号和它们的地址关联起来。这样连接器只要在test.obj的符号导出表中寻找符号f的地址就行了,然后作一些偏移量处理后[因为是将两个.obj文件合并,当然地址会有一定的偏移,这个连接器清楚]写入 main.obj 中的符号导入表中f 所占有的那一项。

====编译模板的关键=========================

这就是大概的过程。其中关键就是:
编译 main.cpp 时,编译器不知道 f 的实现,所有当碰到对它的调用时只是给出一个指示,指示连接器应该为它寻找f的实现体。这也就是说main.obj中没有关于f的任何一行二进制代码。
编译 test.cpp 时,编译器找到了 f 的实现。f 的实现[ 二进制代码 ]出现在test.obj里。
连接时,连接器在test.obj中找到f的实现代码[二进制]的地址[通过符号导出表]。然后将 main.obj 中悬而未决的 call XXX 地址改成f实际的地址。

====解决的办法==========================

然而,对于模板,你知道,模板函数的代码其实并不能直接编译成二进制代码,其中要有一个“具体实现化”的过程。举个例子:
//----------main.cpp------//
template<class T>
void f(T t)
{}
int main()
{
…//do something
f(10); //call f<int> 编译器在这里决定给f一个f<int>的具体实现体
…//do other thing
}

但是,如果你在main.cpp文件中没有调用过f , f 也就得不到具体实现,main.obj 中也就没有关于 f 的任何代码!如果你这样调用了:
f(10); // 则 f<int> 得以具体实现出来
f(10.0); // 则 f<double> 得以具现实现出来
这样main.obj中也就有了 f<int> , f<double> 两个函数的二进制代码段。以此类推。

====解决方法的具体例子========================

这样具体实现化就让编译器知道模板的定义

看下面的例子:[将模板和它的实现分离]
//-------------test.h----------------//
template<class T>
class A
{
public:
void f(); //这里只是个声明
};

//---------------test.cpp-------------//
#include”test.h”
template<class T>
void A<T>::f() //模板的实现,但注意:不是具现
{
…//do something
}

//---------------main.cpp---------------//
#include”test.h”
int main()
{
A<int> a;
a. f();
}

当然编译器在这里也是不知道A<int>::f的定义,因为它不在test.h里面。于是编译器只好希望寄托于连接器,希望它能够在其他.obj里面找到。

A<int>::f的实现体,在本例中就是test.obj。但是,这个 .obj 文件中真有 A<int>::f 的二进制代码吗?

NO!因为C++标准明确表示,当一个模板不被用到的时侯它就不该被具现出来。test.cpp 中没用到了A<int>::f,所以 test.obj 文件中没有A<int>::f的二进制代码。

于是连接器就傻眼了,只好给出一个连接错误。但是,如果在test.cpp中写一个函数,其中调用A<int>::f,则编译器会使其具体实现。有了具体实现,编译器就知道模板的定义了。于是,test.obj 的符号导出表中就有了 A<int>::f 这个符号的地址,于是连接器也就能够完成任务了。




 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值