模板函數(template function)出現編譯連結錯誤(link error)之解析

來源:互聯網
上載者:User

標籤:style   blog   http   color   使用   strong   

總的結論:

   將template function 或者 template class的完整定義直接放在.h檔案中,然後加到要使用這些template function的.cpp檔案中。

 

1. 現象描述

    類似於參考文獻[1],當我們以如下方式使用模板函數時,會出現模板函式宣告、定義分離帶來的連結錯誤:

    

1 // File "foo.h"2 template<typename T>3 extern void foo();
1 // File "foo.cpp"2 #include <iostream>3 #include "foo.h"4 5 template<typename T>6 void foo()7 {8   std::cout << "Here I am!\n";9 }

使用時:

1 // File "main.cpp"2 #include "foo.h"3 4 int main()5 {6   foo<int>();7   ...8 }

在如此使用時,會報出link error.

通常來說有兩種解決辦法:

(1)將template function 或者 template class的完整定義直接放在.h檔案中,然後加到要使用這些template function的.cpp檔案中(當然,此法的確定是,對某些編譯器而言,會造成最後產生的.exe檔案比較大);

 (2)在.cpp檔案中定義模板函數的時候,就將模板函數先執行個體化,例如:

 1 // File "foo.cpp" 2 #include <iostream> 3 #include "foo.h" 4  5 template<typename T> void foo() 6 { 7   std::cout << "Here I am!\n"; 8 } 9 10 template void foo<int>();

或者:把.cpp檔案當成標頭檔一樣加入到需要使用模板函數的.cpp檔案中:

1 // File "main.cpp"2 #include "foo.cpp"//有可能會有點令人迷惑---這是本方法的缺點3 template void foo<int>();4 5 int main()6 {7   foo<int>();8   ...9 }

 

2.原理說明

  參考文獻[3]中已經有很好的說明,此處僅轉載之。

    上面問題不知道怎麼解決,就開始google解決方案: 模板不支援分離編譯, 把你模板類的聲明和實現放到.h檔案裡面 。按照這個說的把.h和.cpp檔案合并後,果然可以了。

但是為什麼呢,為什麼模板就不支援分離編譯?---繼續google ing

搜到了如下文章(文章原文連結:http://blog.csdn.net/bichenggui/article/details/4207084):

首先,一個編譯單元(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中就找不到一行模板執行個體的二進位代碼,於是連接器也黔驢技窮了。

 

Reference

[1]  How can I avoid linker errors with my template functions?  http://www.parashift.com/c++-faq/separate-template-fn-defn-from-decl.html

[2]  C++ template, linking error. http://stackoverflow.com/questions/1353973/c-template-linking-error

[3]  c++模板函式宣告定義分離編譯錯誤詳解. http://www.cnblogs.com/qlwy/archive/2012/03/21/2410045.html 

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.