1. 程式人生 > >C++模板編譯模型

C++模板編譯模型

pre str 查找 enc 生成可執行文件 turn 對象 endif 使用

一:傳統的編譯模型

使用C/C++進行編程時,一般會使用頭文件以使定義和聲明分離,並使得程序以模塊方式組織。將函數聲明、類的定義放在頭文件中,而將函數實現以及類成員函數的定義放在獨立的文件中。

但是對於模板來說,這種方式是行不通的,具體的例子如下:

首先是包含模板聲明的頭文件temp.h:

//temp.h

#ifndef TEMP_H
#define TEMP_H

template<typename T>
int compare(const T &a, const T &b);

template<typename T>
class testtemp
{
public:
    testtemp(const T &a):m_value(a){}
    void display();

private:
    T   m_value;
};

#endif

   該頭文件中包含了一個函數模板的聲明,以及一個類模板的定義。

下面是包含模板定義的源碼文件temp.cpp:

//temp.cpp

#include <iostream>
#include "temp.h"

template<typename T>
int compare(const T &a, const T &b)
{
    if (a < b)  return -1;
    if (b < a)  return 1;
    return 0;
}

template <typename T> 
void testtemp<T>::display()
{
    std::cout << m_value << std::endl;
}

下面是主函數文件main.cpp:

//main.cpp

#include <iostream>
#include "temp.h"

int main()
{
    int a = 1, b = 3;
    int res;

    testtemp<int> tt(4);
    tt.display();

    res = compare(a, b);
    std::cout << "res is " << res << std::endl;
}

  

對上面的文件編譯生成可執行文件時,會報錯:

# g++ -o main main.cpp temp.cpp

/tmp/ccNwfO8x.o: In function `main‘:

main.cpp:(.text+0x47): undefined reference to `testtemp<int>::display()‘

main.cpp:(.text+0x5a): undefined reference to `int compare<int>(int const&, int const&)‘

collect2: error: ld returned 1 exit status

  

報錯的原因如下:

C++中每一個對象所占用的空間大小,都是在編譯的時候就確定的。在編譯階段,源碼文件main.cpp將包含模板聲明的頭文件temp.h包含進來之後,編譯器就需要為main.cpp中涉及到的每個對象生成合適的內存布局,為每個函數生成相應的指令。

當源碼文件main.cpp中涉及到模板類成員函數或者模板函數的調用時,因為模板函數的定義在另一個源碼文件temp.cpp中,編譯器目前僅僅知道它們的聲明。所以,在main.cpp中調用到的的testtemp<int>::display函數,以及int compare<int>(int const&, int const&)函數,編譯器認為這些函數的實現是在其他源碼文件中的,編譯器不會報錯,因為連接器會最終將所有的二進制文件進行連接,從而完成符號查找,形成一個可執行文件。

盡管編譯器也編譯了包含模板定義的源碼文件temp.cpp,但是該文件僅僅是模板的定義,而並沒有真正的實例化出具體的函數來。因此在鏈接階段,編譯器進行符號查找時,發現源碼文件中的符號,在所有二進制文件中都找不到相關的定義,因此就報錯了。

二:模板的編譯模型

當編譯器看到模板定義的時候,它不立即產生代碼。只有在看到用到模板時,如調用了函數模板或調用了類模板的對象的時候,編譯器才產生特定類型的模板實例。

一般而言,當調用函數的時候,編譯器只需要看到函數的聲明。類似地,定義類類型的對象時,類定義必須可用,但成員函數的定義不是必須存在的。因此,應該將類定義和函數聲明放在頭文件中,而普通函數和類成員函數的定義放在源文件中。

模板則不同:要進行實例化,編譯器必須能夠訪問定義模板的源代碼。當調用函數模板或類模板的成員函數的時候,編譯器需要函數定義,需要那些通常放在源文件中的代碼。

標準 C++ 為編譯模板代碼定義了兩種模型。分別是包含編譯模型和分別編譯模型。

所謂包含編譯模型,說白了,就是將函數模板的定義放在頭文件中。因此,對於上面的例子,就是將temp.cpp的內容都放到temp.h中。

包含編譯模型有個問題,如果兩個或多個單獨編譯的源文件使用同一模板,這些編譯器將為每個文件中的模板產生一個實例。因此給定模板會產生多個相同的實例,在鏈接的時候,編譯器會選擇一個實例化而丟棄其他的。

在分別編譯模型中,編譯器會為我們跟蹤相關的模板定義。但是,我們必須讓編譯器知道要記住給定的模板定義,因此需要使用 export 關鍵字。但是,實際上很多編譯器都不支持這個關鍵字,而且C++11 將這個關鍵字設置為 unsued 和 reserved 了。

所以,結論就是,把模板的定義和實現都放到頭文件中。

參考:

http://gaunthan.leanote.com/post/C-%E6%A8%A1%E6%9D%BF%E7%9A%84%E7%BC%96%E8%AF%91%E6%A8%A1%E5%9E%8B

https://www.zhihu.com/question/20630104

C++模板編譯模型