爲何C++編譯器不能支持對模板的分離式編譯

今天在寫隊列模板類的時候,我把模板類的聲明與實現分開寫後,在測試的.cpp文件編譯時報錯「未定義的引用....."c++

在網上查閱得知緣由是:c++編譯器不支持對模板的分離式編譯windows

 

首先,一個編譯單元(translation unit)是指一個.cpp文件以及它所#include的全部.h文件,.h文件裏的代碼將會被擴展到包含它的.cpp文件裏,而後編譯器編譯該.cpp文件爲一個.obj文件(假定咱們的平臺是win32),後者擁有PE(Portable Executable,即windows可執行文件)文件格式,而且自己包含的就已是二進制碼,可是不必定可以執行,由於並不保證其中必定有main函數。當編譯器將一個工程裏的全部.cpp文件以分離的方式編譯完畢後,再由鏈接器(linker)進行鏈接成爲一個.exe文件。函數

 

舉個例子:測試

 

//---------------test.h-------------------//.net

void f();//這裏聲明一個函數fblog

 

//---------------test.cpp--------------//隊列

#include」test.h」編譯器

void f()it

{io

…//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中就找不到一行模板實例的二進制代碼,因而鏈接器也黔驢技窮了。

 

轉自:http://blog.csdn.net/pongba/article/details/19130

相關文章
相關標籤/搜索