您的位置:首页 > 编程语言 > C语言/C++

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

2013-05-10 14:37 190 查看
为什么C++编译器不能支持对模板的分离式编译


刘未鹏(pongba)
C++的罗浮宫(http://blog.csdn.net/pongba)


首先,一个编译单元(translation unit)是指一个.cpp文件以及它所#include的所有.h文件,.h文件里的代码将会被扩展到包含它的.cpp文件里,然后编译器编译该.cpp文件为一个.obj文件(假定我们的平台是win32),后者拥有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中。在main.obj中对f的调用只会生成一行call指令,像这样:



call f [C++中这个名字当然是经过mangling[处理]过的]



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



这个过程如果说的更深入就是:



call f这行指令其实并不是这样的,它实际上是所谓的stub,也就是一个jmp 0xABCDEF。这个地址可能是任意的,然而关键是这个地址上有一行指令来进行真正的call f动作。也就是说,这个.obj文件里面所有对f的调用都jmp向同一个地址,在后者那儿才真正”call”f。这样做的好处就是连接器修改地址时只要对后者的call XXX地址作改动就行了。但是,连接器是如何找到f的实际地址的呢(在本例中这处于test.obj中),因为.obj与.exe的格式是一样的,在这样的文件中有一个符号导入表和符号导出表(import
table和export table)其中将所有符号和它们的地址关联起来。这样连接器只要在test.obj的符号导出表中寻找符号f(当然C++对f作了mangling)的地址就行了,然后作一些偏移量处理后(因为是将两个.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;

f(); // #1

}



编译器在#1处并不知道A<int>::f的定义,因为它不在test.h里面,于是编译器只好寄希望于连接器,希望它能够在其他.obj里面找到A<int>::f的实例,在本例中就是test.obj,然而,后者中真有A<int>::f的二进制代码吗?NO!!!因为C++标准明确表示,当一个模板不被用到的时侯它就不该被实例化出来,test.cpp中用到了A<int>::f了吗?没有!!所以实际上test.cpp编译出来的test.obj文件中关于A::f一行二进制代码也没有,于是连接器就傻眼了,只好给出一个连接错误。但是,如果在test.cpp中写一个函数,其中调用A<int>::f,则编译器会将其实例化出来,因为在这个点上(test.cpp中),编译器知道模板的定义,所以能够实例化,于是,test.obj的符号导出表中就有了A<int>::f这个符号的地址,于是连接器就能够完成任务。



关键是:在分离式编译的环境下,编译器编译某一个.cpp文件时并不知道另一个.cpp文件的存在,也不会去查找(当遇到未决符号时它会寄希望于连接器)。这种模式在没有模板的情况下运行良好,但遇到模板时就傻眼了,因为模板仅在需要的时候才会实例化出来,所以,当编译器只看到模板的声明时,它不能实例化该模板,只能创建一个具有外部连接的符号并期待连接器能够将符号的地址决议出来。然而当实现该模板的.cpp文件中没有用到模板的实例时,编译器懒得去实例化,所以,整个工程的.obj中就找不到一行模板实例的二进制代码,于是连接器也黔驴技穷了。



分享到:

上一篇:Enhanced Assertions
下一篇:在C++中侦测内嵌型别的存在(rev#2)

后者拥有PE[Portable Executable,即windows可执行文件]文件格式 <br>————————————————————————————— <br>这句话有点不太精确,c++编译成的可执行文件有很多种,一般windows上是PE,unix上是COFF, <br> <br>这句话说的是有问题的, Windows 下的OBJ 文件格式有很多种, 比如intel 和 borland 用的
OBJ 文件格式就是OMF格式, 跟 microsoft 的就不一样, 微软采用的COFF格式. <br> <br>只有生成可执行文件的时候,才对格式有限制,必须采用PE格式.

8楼 刘未鹏 2005-04-25 13:31发表 [回复] [引用] [举报]


to PM: <br>你说得对:-) <br>另外,C++编译器不支持export是有充足的理由的,这个特性很可能会从C++标准里消失,你可以参考C++标准提案之一:-)里面详细解释了这个特性:-)

7楼 dawndu 2005-04-24 20:40发表 [回复]


后者拥有PE[Portable Executable,即windows可执行文件]文件格式 <br>————————————————————————————— <br>这句话有点不太精确,c++编译成的可执行文件有很多种,一般windows上是PE,unix上是COFF,但是文中所讲让人以为都是PE格式的。不过全文分析的很全面,chilly说的也很对,export很多编译器不支持的。

6楼 chilly 2004-12-27 11:39发表 [回复]


关于模板的分离式编译可以在定义模板的.cpp文件中的函数前加上 <br>export,即: <br>//---------------test.cpp-------------// <br> <br> #include”test.h” <br> <br> export template<class T> <br> <br> void A<T>::f() //模板的实现,但注意:不是具现 <br> <br> { <br> <br> …//do something <br> <br> }
<br>不过很遗憾,一般的编辑器不支持export <br> <br>

5楼 刘未鹏 2004-09-07 22:54发表 [回复]


C++ 98 . 4.10 : <br>A null pointer constant is an integral constant expression (5.19) rvalue of integer type that evaluates to <br>zero. A null pointer constant can be converted to a pointer type; <br> <br>The C++ Programming Language -- bjarne stroustrup:
<br>In C, it has been popular to define a macro NULL to represent the zero pointer. Because of <br>C++’s tighter type checking, the use of plain 0, rather than any suggested NULL macro, leads to <br>fewer problems. If you feel you must define NULL, use <br>const
int NULL = 0; <br> <br>其实stroustrup的意思是,标准保证了常量0可以被转化为任何指针类型,但是如果用NULL,可能会被定义为(void*)0,从而需要一个static_cast,这是因为C++的类型系统更为严格: <br> static_cast<Target>(NULL)

4楼 li_yadan 2004-09-07 03:04发表 [回复]


楼主分析得很透彻!精彩!正好回答困惑我多日的问题,十分感谢! <br> <br>另外,我在编c++模板程序的时候,发现有时候用NULL指针出现问题(比如构造函数把某个XNode<T>*类型的成员变量赋值为NULL时编译老说类型不对),但直接赋值为0反而可以,不知道楼主遇到过没有,另外对C++的NULL,楼主有什么经验,很想听听!

3楼 li_yadan 2004-09-07 02:57发表 [回复]


Thinking in C++第一版15章P300 <br> <br>头文件 <br>甚至是在定义非内联函数时,模板的头文件中也会放置所有的声明和定义。这似乎违背了通 <br>常的头文件规则:“不要在分配存储空间前放置任何东西”,这条规则是为了防止在连接时的多重定 <br>义错误。但模板定义很特殊。由t e m p l a t e <?>处理的任何东西都意味着编译器在当时不为它分配存 <br>储空间,它一直处于等待状态直到被一个模板实例告知。在编译器和连接器的某一处,有一机制能 <br>去掉指定模板的多重定义。所以为了容易使用,几乎总是在头文件中放置全部的模板声明和定义。
<br>有时,也可能为了满足特殊的需要(例如,强制模板实例仅存在于一简单的Windows DLL <br>文件中)而要在一个独立的C P P文件中放置模板的定义。大多数编译器有一些机制允许这么做, <br>那么我们就必须检查我们特定的编译器说明文档以便使用它。

2楼 li_yadan 2004-09-07 10:28发表 [回复]


谢谢楼主回答。 <br>既然NULL是define成0的,那么用NULL的地方是不是都直接写0就好。 <br> <br>有人告诉我:“任何指针可以用0来赋值的,这是一种标准转换.像任何指针都可以转换成void *一样也是标准转换”。我想,如果0是c++标准规定可以赋值给指针的,那我们是不是可以不用麻烦地include stdio.h或者iostream.h(不是这些库也会被STL库取代吗),而直接用0来代替空指针? 这样作会有什么坏处? <br> <br>我的c++经验很少,不知道目前实际工程中是如何处理NULL值的。其实我担心引用库不一样,NULL可能被redefine了。或者0赋给指针不被有的编译器支持。(另外,看《c++程序设计语言》里面偶尔提到“声名狼藉的NULL问题”)
楼主你对Boost库剖析颇深,像我这样,完成一个数据结构程序,new一个ListNode时,其中next指针初始化为NULL,0还是void*?给点意见:) <br> <br>

1楼 刘未鹏 2004-09-07 10:04发表 [回复]


你的问题可能是这样的: <br> <br> NULL本身是定义在stdio.h头文件里的,你可能没有include<stdio.h>,其定义是这样的: <br> #ifdef __cplusplus <br> #define NULL 0 <br> ... <br> <br> 其实NULL就是0。并且,NULL从语义上说,就是“空指针”,没有理由不可以赋值给一个指针的!
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: