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