為什麼C++編譯器不能支援對模板的分離式編譯 (轉)

worldblog發表於2007-08-17
為什麼C++編譯器不能支援對模板的分離式編譯 (轉)[@more@]

  為什麼C++不能支援對模板的分離式編譯

  --ppLiu

首先,C++標準中提到,一個編譯單元[translation unit]是指一個.cpp以及它所include的所有.h檔案,.h檔案裡的程式碼將會被擴充套件到包含它的.cpp檔案裡,然後編譯器編譯該.cpp檔案為一個.obj檔案,後者擁有PE[Portable Executable,即可檔案]檔案格式,並且本身包含的就已經是二進位制碼,但是,不一定能夠執行,因為並不保證其中一定有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 0x23423[這個地址可能是任意的,然而關鍵是這個地址上有一行指令來進行真正的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實際的地址。

  完成。

 :namespace prefix = o ns = "urn:schemas--com::office" />

然而,對於模板,你知道,模板函式的程式碼其實並不能直接編譯成二進位制程式碼,其中要有一個“具現化”的過程。舉個例子:

//----------main.cpp------//

 template

 void f(T t)

 {}

 int main()

 {

   …//do something

  f(10); //call f 編譯器在這裡決定給f一個f的具現體

  …//do other thing

  }

也就是說,如果你在main.cpp檔案中沒有呼叫過f,f也就得不到具現,從而main.obj中也就沒有關於f的任意一行二進位制程式碼!!如果你這樣呼叫了:

  f(10); //f得以具現化出來

f(10.0); //f得以具現化出來

這樣main.obj中也就有了f,f兩個函式的二進位制程式碼段。以此類推。

然而具現化要求編譯器知道模板的定義,不是嗎?

看下面的例子:[將模板和它的實現分離]

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

  template

  class A

  {

  public:

  void f(); //這裡只是個宣告

  };

//---------------test.cpp-------------//

  #include”test.h”

  template

  void A::f()  //模板的實現,但注意:不是具現

  {

  …//do something

  }

//---------------main.cpp---------------//

  #include”test.h”

  int main()

  {

  A a;

a.  f(); //編譯器在這裡並不知道A::f的定義,因為它不在test.h裡面

  //於是編譯器只好寄希望於聯結器,希望它能夠在其他.obj裡面找到

  //A::f的實現體,在本例中就是test.obj,然而,後者中真有A::f的

  //二進位制程式碼嗎?NO!!!因為C++標準明確表示,當一個模板不被用到的時

  //侯它就不該被具現出來,test.cpp中用到了A::f了嗎?沒有!!所以實

  //際上test.cpp編譯出來的test.obj檔案中關於A::f的一行二進位制程式碼也沒有

  //於是聯結器就傻眼了,只好給出一個連線錯誤

  //但是,如果在test.cpp中寫一個函式,其中呼叫A::f,則編譯器會將其//具現出來,因為在這個點上[test.cpp中],編譯器知道模板的定義,所以能//夠具現化,於是,test.obj的符號匯出表中就有了A::f這個符號的地

//址,於是聯結器就能夠完成任務。

  }

關鍵是:在分離式編譯的環境下,編譯器編譯某一個.cpp檔案時並不知道另一個.cpp檔案的存在,也不會去查詢[當遇到未決符號時它會寄希望於聯結器]。這種在沒有模板的情況下執行良好,但遇到模板時就傻眼了,因為模板僅在需要的時候才會具現化出來,所以,當編譯器只看到模板的宣告時,它不能具現化該模板,只能建立一個具有外部連線的符號並期待聯結器能夠將符號的地址決議出來。然而當實現該模板的.cpp檔案中沒有用到模板的具現體時,編譯器懶得去具現,所以,整個工程的.obj中就找不到一行模板具現體的二進位制程式碼,於是聯結器也傻眼了!!!!!!!!!!!!!!!!!!!!!!!!!

 

 


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/10752043/viewspace-962857/,如需轉載,請註明出處,否則將追究法律責任。

相關文章