C++ 模板類的聲明與實(shí)現(xiàn)分離問(wèn)題

模版的編譯

c++ primer p657

  • 一般來(lái)說(shuō),如果你的項(xiàng)目沒有混合使用 C 和 C++ 語(yǔ)言,那么你使用 .h 和 .cpp 是沒有問(wèn)題的。否則你將C和C++的頭文件進(jìn)行分離,因?yàn)橥ǔN覀儼袰和C++分離編譯,再統(tǒng)一鏈接;
  • 函數(shù)經(jīng)過(guò)編譯系統(tǒng)的翻譯成匯編,函數(shù)名對(duì)應(yīng)著匯編標(biāo)號(hào)。 因?yàn)镃編譯函數(shù)名與得到的匯編代號(hào)基本一樣,如:fun()=>_fun, main=>_main ;但是C++中函數(shù)名與得到的匯編代號(hào)有比較大的差別。 如:由于函數(shù)重載,函數(shù)名一樣,但匯編代號(hào)絕對(duì)不能一樣。
  • 為了區(qū)分,編譯器會(huì)把函數(shù)名和參數(shù)類型合在一起作為匯編代號(hào), 這樣就解決了重載問(wèn)題。具體如何把函數(shù)名和參數(shù)類型合在一起, 要看編譯器的幫助說(shuō)明了。
  • 這樣一來(lái),如果C++調(diào)用C,如fun(),則調(diào)用名就不是C的翻譯結(jié)果_fun, 而是帶有參數(shù)信息的一個(gè)名字,因此就不能調(diào)用到fun(),為了解決 這個(gè)問(wèn)題,加上extern "C"表示該函數(shù)的調(diào)用規(guī)則是C的規(guī)則,則調(diào)用 時(shí)就不使用C++規(guī)則的帶有參數(shù)信息的名字,而是_fun,從而達(dá)到調(diào)用 C函數(shù)的目的。

具體看下面的栗子:

//test.h
#ifndef ALGORITHMS_TEST_H
#define ALGORITHMS_TEST_H

template <typename T>
class Foo
{
    T bar;
public:
    //void doSomething(T param) {/* do stuff using T */}
    void doSomething(T param);
};


#endif //ALGORITHMS_TEST_H
//test.cpp
#include<iostream>
#include "test.h"

template <typename T>
void  Foo<T>::doSomething(T param) {
    std::cout << param << std::endl;
}
//main.cpp
#include "test.h"
int main() {
    Foo<int> f;
    f.doSomething(3);
    return 0;
}

編譯報(bào)連接錯(cuò)誤
ld: symbol(s) not found for architecture x86_64
clang: error: linker command failed with exit code 1 (use -v to see invocation)
make[3]: *** [Algorithms] Error 1

為什么模版類的聲明和實(shí)現(xiàn)分開會(huì)出現(xiàn)連接問(wèn)題呢

因?yàn)樾枰獑为?dú)編譯,并且模板是實(shí)例化樣式的多態(tài)性;
在實(shí)例化模板時(shí),編譯器會(huì)使用給定的模板參數(shù)創(chuàng)建一個(gè)新類。例如:

template<typename T>
struct Foo
{
    T bar;
    void doSomething(T param) {/* do stuff using T */}
};

// somewhere in a .cpp
Foo<int> f; 

閱讀此行時(shí),編譯器將創(chuàng)建一個(gè)新類(我們稱之為FooInt),其等效于以下內(nèi)容:

struct FooInt
{
    int bar;
    void doSomething(int param) {/* do stuff using int */}
}

因此,編譯器需要訪問(wèn)方法的實(shí)現(xiàn),以使用template參數(shù)實(shí)例化它們(在這種情況下為int)。如果這些實(shí)現(xiàn)不在頭文件中,則將無(wú)法訪問(wèn)它們,因此編譯器將無(wú)法實(shí)例化模板。

具體解釋如下:

foo.h
    聲明的接口 class MyClass<T>
foo.cpp
    定義執(zhí)行 class MyClass<T>
bar.cpp
    用途 MyClass<int>
  • 單獨(dú)編譯意味著我應(yīng)該能夠獨(dú)立于bar.cpp編譯foo.cpp。編譯器在每個(gè)編譯單元上完全獨(dú)立地完成分析、優(yōu)化和代碼生成的所有艱苦工作;我們不需要做整個(gè)程序分析。只有鏈接器需要一次處理整個(gè)程序,鏈接器的工作大大簡(jiǎn)化了。
  • 當(dāng)我編譯foo.cpp時(shí),bar.cpp甚至不需要存在,但是我仍然應(yīng)該可以將foo.o與bar.o一起使用。剛剛生成的文件,而無(wú)需重新編譯foo.cpp。甚至可以將foo.cpp編譯成動(dòng)態(tài)庫(kù),而無(wú)需foo.cpp即可將其分發(fā)到其他位置,并與在我編寫foo.cpp之后多年編寫的代碼鏈接。
  • "實(shí)例化樣式多態(tài)性”表示模板MyClass<T>并不是真正的通用(范型)類,它不能被編譯成可以處理任何T值的代碼。這將增加開銷,如裝箱,需要傳遞函數(shù)指針到分配器和構(gòu)造器,等等。c++模板的目的是避免編寫幾乎相同的類MyClass_int、類MyClass_float等,但仍然能夠編譯代碼,結(jié)束了就像我們分別編寫每個(gè)版本一樣。因此,模板實(shí)際上是模板。類模板不是類,而是為T我們遇到的每個(gè)類創(chuàng)建新類的秘訣。模板不能被編譯成代碼,只有實(shí)例化模板的結(jié)果可以被譯。
  • 因此,在編譯foo.cpp時(shí),編譯器無(wú)法通過(guò)看到bar.cpp來(lái)知道需要MyClass<int>。它可以看到模板MyClass<T>,但不能為此編譯出相應(yīng)的代碼(它是模板,而不是類)。并且在編譯bar.cpp時(shí),編譯器可以看到它需要?jiǎng)?chuàng)建一個(gè)MyClass<int>,但是看不到該模板MyClass<T>(只能在foo.h中看到其接口,不能看到具體類成員函數(shù)的具體實(shí)現(xiàn)等),因此無(wú)法創(chuàng)建它。
  • 如果Foo.cpp中本身使用MyClass<int>,將在編譯時(shí)會(huì)產(chǎn)生對(duì)應(yīng)代碼在Foo.cpp中,因此當(dāng)文件bar.o鏈接到文件foo.o他們可以連接并工作。我們可以利用這一事實(shí),通過(guò)編寫單個(gè)模板,在.cpp文件中實(shí)現(xiàn)一組有限的模板實(shí)例化。但bar.cpp無(wú)法將模板作為模板實(shí)例化它的類型;它只能使用foo.cpp的作者認(rèn)為提供的模板化類的已存在版本。
  • 您可能會(huì)認(rèn)為,在編譯模板時(shí),編譯器應(yīng)“生成所有版本”,并且在鏈接過(guò)程中會(huì)濾除從未使用過(guò)的版本。除了龐大的開銷和極端的困難之外,這種方法還會(huì)面臨困難,因?yàn)橹羔樅蛿?shù)組之類的“類型修飾符”功能甚至允許內(nèi)置類型產(chǎn)生無(wú)限數(shù)量的類型,當(dāng)我現(xiàn)在擴(kuò)展程序時(shí)會(huì)發(fā)生什么通過(guò)添加:
baz.cpp
    聲明并實(shí)現(xiàn)class BazPrivate,并使用MyClass<BazPrivate>

除非我們:

  • 1、每當(dāng)我們改變程序中的任何其他文件時(shí),必須重新編譯foo.cpp,以防它添加了一個(gè)新的實(shí)例化MyClass<T>
  • 2、要求baz.cpp包含(可能通過(guò)標(biāo)頭包含)的完整模板MyClass<T>,以便編譯器可以MyClass<BazPrivate>在編譯baz.cpp時(shí)生成。

沒有人喜歡(1),因?yàn)檎麄€(gè)程序分析的編譯系統(tǒng)需要很長(zhǎng)時(shí)間來(lái)編譯,而且如果沒有源代碼,就不可能分發(fā)已編譯的庫(kù)。所以我們用(2)代替。

解決方案

  • 常見的解決方案是將模板聲明寫入頭文件中,然后在實(shí)現(xiàn)文件中實(shí)現(xiàn)該類(例如.tpp),并在頭末尾包含該實(shí)現(xiàn)文件。
//test.h
template <typename T>
struct Foo
{
    void doSomething(T param);
};

#include "test.tpp"
//test.tpp
template <typename T>
void Foo<T>::doSomething(T param)
{
    //implementation
}

這樣,實(shí)現(xiàn)仍與聲明分開,但編譯器可以訪問(wèn);

另外一種代替的解決方案:使實(shí)現(xiàn)分離,并顯式實(shí)例化所需的所有模板實(shí)例:

//Foo.h
//no implementation
//template <typename T> struct Foo { ... };
template <typename T>
class Foo
{
    T bar;
public:
    //void doSomething(T param) {/* do stuff using T */}
    void doSomething(T param);
};
//Foo.cpp
// implementation of Foo's methods
// explicit instantiations

template <typename T>
void  Foo<T>::doSomething(T param) {
    std::cout << param << std::endl;
}

template class Foo<int>;  //相關(guān)的實(shí)例化代碼由編譯器產(chǎn)生
template class Foo<float>;
//main.cpp
#include "test.hpp"
int main(int argc, const char * argv[]) {   
 
    Foo<int> f;
    f.doSomething(3);
    
    return 0;
    
}

/* 編譯運(yùn)行成功:
    3  Program ended with exit code: 0
*/

參考:https://stackoverflow.com/questions/495021/why-can-templates-only-be-implemented-in-the-header-file

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容