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

模版的編譯

c++ primer p657

  • 一般來說,如果你的項目沒有混合使用 C 和 C++ 語言,那么你使用 .h 和 .cpp 是沒有問題的。否則你將C和C++的頭文件進行分離,因為通常我們把C和C++分離編譯,再統(tǒng)一鏈接;
  • 函數(shù)經(jīng)過編譯系統(tǒng)的翻譯成匯編,函數(shù)名對應著匯編標號。 因為C編譯函數(shù)名與得到的匯編代號基本一樣,如:fun()=>_fun, main=>_main ;但是C++中函數(shù)名與得到的匯編代號有比較大的差別。 如:由于函數(shù)重載,函數(shù)名一樣,但匯編代號絕對不能一樣。
  • 為了區(qū)分,編譯器會把函數(shù)名和參數(shù)類型合在一起作為匯編代號, 這樣就解決了重載問題。具體如何把函數(shù)名和參數(shù)類型合在一起, 要看編譯器的幫助說明了。
  • 這樣一來,如果C++調(diào)用C,如fun(),則調(diào)用名就不是C的翻譯結果_fun, 而是帶有參數(shù)信息的一個名字,因此就不能調(diào)用到fun(),為了解決 這個問題,加上extern "C"表示該函數(shù)的調(diào)用規(guī)則是C的規(guī)則,則調(diào)用 時就不使用C++規(guī)則的帶有參數(shù)信息的名字,而是_fun,從而達到調(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;
}

編譯報連接錯誤
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

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

因為需要單獨編譯,并且模板是實例化樣式的多態(tài)性;
在實例化模板時,編譯器會使用給定的模板參數(shù)創(chuàng)建一個新類。例如:

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

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

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

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

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

具體解釋如下:

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

除非我們:

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

沒有人喜歡(1),因為整個程序分析的編譯系統(tǒng)需要很長時間來編譯,而且如果沒有源代碼,就不可能分發(fā)已編譯的庫。所以我們用(2)代替。

解決方案

  • 常見的解決方案是將模板聲明寫入頭文件中,然后在實現(xiàn)文件中實現(xiàn)該類(例如.tpp),并在頭末尾包含該實現(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
}

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

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

//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>;  //相關的實例化代碼由編譯器產(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;
    
}

/* 編譯運行成功:
    3  Program ended with exit code: 0
*/

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

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

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

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