C++將模板實現(xiàn)放入頭文件原理解析
寫在前面
本文通過實例分析與講解,解釋了為什么C++一般將模板實現(xiàn)放在頭文件中。這主要與C/C++的編譯機制以及C++模板的實現(xiàn)原理相關(guān),詳情見正文。同時,本文給出了不將模板實現(xiàn)放在頭文件中的解決方案。
例子
現(xiàn)有如下3個文件:
// add.h template <typename T> T Add(const T &a, const T &b); // add.cpp #include "add.h" template <typename T> T Add(const T &a, const T &b) { return a + b; } // main.cpp #include "add.h" #include <iostream> int main() { int res = Add<int>(1, 2); std::cout << res << "\n"; return 0; }
現(xiàn)象
使用 g++ -c add.cpp 編譯生成 add.o ,使用 g++ -c main.cpp 編譯生成 main.o ,這兩步都沒有問題。
使用 g++ -o main.exe main.o add.o 生成 main.exe 時,報錯 undefined reference to 'int Add(int const&, int const&)' 。
當然,直接 g++ add.cpp main.cpp -o main.exe 肯定也會報錯,這里把編譯和鏈接分開是為了更好地展示與分析問題。?
原因
出現(xiàn)上述問題的原因是:
(1)C/C++源文件是按編譯單元(translation unit)分開、獨立編譯的。所謂translation unit,其實就是輸入給編譯器的source code,只不過該source code是經(jīng)過預(yù)處理(pre-processed?,包括去掉注釋、宏替換、頭文件展開)的。在本例中,即便你使用 g++ add.cpp main.cpp -o main.exe ,編譯器也是分別編譯 add.cpp 和 main.cpp (注意是預(yù)處理后的)的。在編譯 add.cpp 時,編譯器根本感知不到 main.cpp 的存在,反之同理。
(2) C++模板是通過實例化(instantiation)來實現(xiàn)多態(tài)(polymorphism)的。以函數(shù)模板為例,首先需要區(qū)分“函數(shù)模板”和“模板函數(shù)”。本例中,上面代碼的第8~12行是函數(shù)模板,顧名思義,它就是一個模子,不是具體的函數(shù),是不能運行的;當用具體的類型,如 int ,實例化模板參數(shù) T 后,會生成函數(shù)模板的一個具體實例,稱為模板函數(shù),這是真正可以運行的函數(shù)。“函數(shù)模板”和“模板函數(shù)”的關(guān)系,可以類比“類”和“對象”的關(guān)系。以 int 為例,生成的實例/模板函數(shù)大概長這樣(細節(jié)上肯定和編譯器的實際實現(xiàn)有出入,但核心意思不會變)。
int Add_int_int(const int &a, const int &b) { return a + b; }
?對于每一個用到的具體類型,編譯器都會生成對應(yīng)版本的實例,當函數(shù)調(diào)用時,會調(diào)用到該實例。如用到了 Add<int> ,就會生成 Add_int_int ,用到了 Add<double> ,就會生成 Add_double_double ,等等。本例中,當編譯器編譯到第20行,即 int res = Add<int>(1,2); 一句時,編譯器就會試圖生成 int 版本的模板實例(即模板函數(shù))。
(3)編譯器為模板生成實例的必要條件是:1. 知道模板的具體定義/實現(xiàn);2. 知道模板參數(shù)對應(yīng)的實際類型。
分析
下面把上面兩節(jié)內(nèi)容結(jié)合起來分析。
(1)當編譯 add.cpp 時,相當于編譯
template <typename T> T Add(const T &a, const T &b); template <typename T> T Add(const T &a, const T &b) { return a + b; }
此時編譯器雖然知道模板的具體定義,卻不知道模板參數(shù) T 的具體類型,因此不會生成任何的實例化代碼。
(2)當編譯 main.cpp 時,相當于編譯
#include <iostream> template <typename T> T Add(const T &a, const T &b); int main() { int res = Add<int>(1, 2); std::cout << res << "\n"; return 0; }
當編譯到 int res = Add<int>(1, 2); 時,編譯器想要生成 int 版本的函數(shù)實例,但它找不到函數(shù)模板的具體定義(即 Add 的“函數(shù)體”),只好作罷。好在編譯器看到了函數(shù)模板的聲明,于是通過了編譯,將尋找 int 版本函數(shù)實例的任務(wù)留給了鏈接器。?
至此,編譯 add.cpp 時,只知模板定義,不知模板類型參數(shù),無法生成具體的函數(shù)定義;編譯 main.cpp 時,只知模板類型參數(shù),不知模板定義,同樣無法生成具體的函數(shù)定義。?
(3)沒什么好說的,鏈接器在 add.o 和 main.o 中都沒找到 int 版本的 Add 定義,直接報錯。?
解決方案
方案一
傳統(tǒng)方法:把模板實現(xiàn)也放在頭文件中。
// add.h template <typename T> T Add(const T &a, const T &b) { return a + b; } // main.cpp #include "add.h" #include <iostream> int main() { int res = Add<int>(1, 2); std::cout << res << "\n"; return 0; }
當編譯 main.cpp 時,相當于編譯?
#include <iostream> template <typename T> T Add(const T &a, const T &b) { return a + b; } int main() { int res = Add<int>(1, 2); std::cout << res << "\n"; return 0; }
此時編譯器既知道函數(shù)模板的定義,又知道具體的模板類型參數(shù) int ,因此可以生成 int 版本的函數(shù)實例,不會出錯。?
這種方式的優(yōu)缺點如下:
- 優(yōu)點:可以按需生成。假如我們在 main.cpp 中調(diào)用了 Add<double>(1.0, 2.0); ,編譯器就會為我們生成 double 版本的函數(shù)實例。
- 缺點:不得不把實現(xiàn)細節(jié)暴露給用戶。
方案二
模板聲明和定義分離的方案。?
// add.h template <typename T> T Add(const T &a, const T &b); // add.cpp #include "add.h" template <typename T> T Add(const T &a, const T &b) { return a + b; } template int Add(const int &a, const int &b); // main.cpp #include "add.h" #include <iostream> int main() { int res = Add<int>(1, 2); std::cout << res << "\n"; return 0; }
注意, template int Add(const int &a, const int &b); 是函數(shù)模板實例化(function template instantiation)[1], template 關(guān)鍵字不能省略,否則, int Add(const int &a, const int&b); 會被編譯器當做普通函數(shù)的聲明,從而在鏈接時又會報 undefined reference to 'int Add(int const&, int const&)' 錯誤。?
對于這種寫法,編譯器在編譯 add.cpp 時,既能看到函數(shù)模板的定義,又能看到具體的模板類型參數(shù) int ,于是生成了 int 版本的函數(shù)實例,整個程序可以正常編譯運行。?
很顯然,這種情況下編譯器只生成了 int 版本的函數(shù)實例,所以,在 main.cpp 中使用 Add<double>(1.0, 2.0); 這樣的代碼肯定是不可以的。這種情況的優(yōu)缺點可以辯證看待:?
優(yōu)點:
- 1. 可以隱藏實現(xiàn)細節(jié)(我們可以把 add.cpp 做成.lib或.dll);
- 2. 也可以限制只實例化特定的版本。?
缺點:就是只能使用特定的幾個版本,不能像方案一那樣在編譯 main.cpp 時根據(jù)具體的調(diào)用情況按需生成。?
從這里也可以看出,模板實現(xiàn)不一定非得放在頭文件中。
參考
[1] Function template - cppreference.com
[2] c++ - Why can templates only be implemented in the header file? - Stack Overflow
寫在后面
本文從C/C++編譯機制以及C++模板實現(xiàn)原理的角度,結(jié)合具體實例,講解了為什么一般將模板實現(xiàn)放在頭文件中。由于在下才疏學(xué)淺,能力有限,錯誤疏漏之處在所難免,懇請廣大讀者批評指正,您的批評是在下前進的不竭動力,更多關(guān)于C++頭文件放入模板實現(xiàn)的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
opencv實現(xiàn)機器視覺檢測和計數(shù)的方法
在機器視覺中,有時需要對產(chǎn)品進行檢測和計數(shù)。其難點無非是對于產(chǎn)品的圖像分割。本文就來介紹一下機器視覺檢測和計數(shù)的實現(xiàn),感興趣的可以參考一下2021-05-05C++如何比較兩個字符串或string是否相等strcmp()和compare()
這篇文章主要介紹了C++如何比較兩個字符串或string是否相等strcmp()和compare()問題,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2023-11-11