C++模板的聲明和實現爲什麼要放在頭文件中?

問題的產生
經過下例來講明問題。例如在array.h文件中有模板類array:
// array.h
template <typename T, int SIZE>
class array
{
    T data_[SIZE];
    array (const array& other);
    const array& operator = (const array& other);
public:
    array(){};
    T& operator[](int i) {return data_[i];}
    const T& get_elem (int i) const {return data_[i];}
    void set_elem(int i, const T& value) {data_[i] = value;}
    operator T*() {return data_;}      
};            
            
而後在main.cpp文件中的主函數中使用上述模板:
// main.cpp
#i nclude "array.h"ios

int main(void)
{
array<int, 50> intArray;
intArray.set_elem(0, 2);
int firstElem = intArray.get_elem(0);
int* begin = intArray;
}
        
這時編譯和運行都是正常的。程序先建立一個含有50個整數的數組,而後設置數組的第一個元素值爲2,再讀取第一個元素值,最後將指針指向數組起點。程序員

但若是用傳統編程方式來編寫會發生什麼事呢?咱們來看看:編程

將array.h文件分裂成爲array.h和array.cpp二個文件(main.cpp保持不變)
// array.h        
template <typename T, int SIZE>
class array
{
      T data_[SIZE];
      array (const array& other);
      const array& operator = (const array& other);
  public:
      array(){};
      T& operator[](int i);
      const T& get_elem (int i) const;
      void set_elem(int i, const T& value);
      operator T*();      
};        
    
// array.cpp
#i nclude "array.h"windows

template<typename T, int SIZE> T& array<T, SIZE>::operator [](int i)
    {
    return data_[i];
    }數組

template<typename T, int SIZE> const T& array<T, SIZE>::get_elem(int i) const
    {
    return data_[i];
    }ide

template<typename T, int SIZE> void array<T, SIZE>::set_elem(int i, const T& value)
    {
    data_[i] = value;
    }
template<typename T, int SIZE> array<T, SIZE>::operator T*()
    {
    return data_;
    }
        
編譯時會出現3個錯誤。問題出來了:
  爲何錯誤都出如今第一個地方?
  爲何只有3個連接出錯?array.cpp中有4個成員函數。
  
要回答上面的問題,就要深刻了解模板的實例化過程。函數

模板實例化
程序員在使用模板類時最常犯的錯誤是將模板類視爲某種數據類型。所謂類型參量化(parameterized types)這樣的術語致使了這種誤解。模板固然不是數據類型,模板就是模板,恰如其名:ui

  編譯器使用模板,經過更換模板參數來建立數據類型。這個過程就是模板實例化(Instantiation)。
  從模板類建立獲得的類型稱之爲特例(specialization)。 
  模板實例化取決於編譯器可以找到可用代碼來建立特例(稱之爲實例化要素,
  point of instantiation)。
  要建立特例,編譯器不但要看到模板的聲明,還要看到模板的定義。
  模板實例化過程是遲鈍的,即只能用函數的定義來實現實例化。spa


再回頭看上面的例子,能夠知道array是一個模板,array<int, 50>是一個模板實例 - 一個類型。從array建立array<int, 50>的過程就是實例化過程。實例化要素體如今main.cpp文件中。若是按照傳統方式,編譯器在array.h文件中看到了模板的聲明,但沒有 模板的定義,這樣編譯器就不能建立類型array<int, 50>。但這時並不出錯,由於編譯器認爲模板定義在其它文件中,就把問題留給連接程序處理。.net

如今,編譯array.cpp時會發生什麼問題呢?編譯器能夠解析模板定義並檢查語法,但不能生成成員函數的代碼。它沒法生成代碼,由於要生成代碼,須要知道模板參數,即須要一個類型,而不是模板自己。

這樣,連接程序在main.cpp 或 array.cpp中都找不到array<int, 50>的定義,因而報出無定義成員的錯誤。

至此,咱們回答了第一個問題。但還有第二個問題,在array.cpp中有4個成員函數,連接器爲何只報了3個錯誤?回答是:實例化的惰性致使這種現象。在main.cpp中尚未用上operator[],編譯器尚未實例化它的定義。

解決方法
認識了問題,就可以解決問題:
  在實例化要素中讓編譯器看到模板定義。
  用另外的文件來顯式地實例化類型,這樣連接器就能看到該類型。
  使用export關鍵字。

前二種方法一般稱爲包含模式,第三種方法則稱爲分離模式。

第一種方法意味着在使用模板的轉換文件中不但要包含模板聲明文件,還要包含模板定義文件。在上例中,就是第一個示例,在array.h中用行內函數 定義了全部的成員函數。或者在main.cpp文件中也包含進array.cpp文件。這樣編譯器就能看到模板的聲明和定義,並由今生成 array<int, 50>實例。這樣作的缺點是編譯文件會變得很大,顯然要下降編譯和連接速度。

第二種方法,經過顯式的模板實例化獲得類型。最好將全部的顯式實例化過程安放在另外的文件中。在本例中,能夠建立一個新文件templateinstantiations.cpp:
// templateinstantiations.cpp                
#i nclude "array.cpp"

template class array <int, 50>; // 顯式實例化
        
array<int, 50>類型不是在main.cpp中產生,而是在templateinstantiations.cpp中產生。這樣連接器就可以找到它的定義。用 這種方法,不會產生巨大的頭文件,加快編譯速度。並且頭文件自己也顯得更加「乾淨」和更具備可讀性。但這個方法不能獲得惰性實例化的好處,即它將顯式地生 成全部的成員函數。另外還要維護templateinstantiations.cpp文件。

第三種方法是在模板定義中使用export關鍵字,剩下的事就讓編譯器去自行處理了。當我在
Stroustrup的書中讀到export 時,感到很是興奮。但很快就發現VC 6.0不支持它,後來又發現根本沒有編譯器可以支持這個關鍵字(第一個支持它的編譯器要在2002年末才問世)。自那之後,我閱讀了很多關於export 的文章,瞭解到它幾乎不能解決用包含模式可以解決的問題。欲知更多的export關鍵字,建議讀讀Herb Sutter撰寫的文章。

結論
要開發模板庫,就要知道模板類不是所謂的"原始類型",要用其它的編程思路。本文目的不是要嚇唬那些想進行模板編程的程序員。偏偏相反,是要提醒他們避免犯下開始模板編程時都會出現的錯誤。


//////////////////////////////
http://www.cnblogs.com/xgchang/archive/2004/11/12/63139.aspx 
甚 至是在定義非內聯函數時,模板的頭文件中也會放置全部的聲明和定義。這彷佛違背了一般的頭文件規則:「不要在分配存儲空間前放置任何東西」,這條規則是爲 了防止在鏈接時的多重定義錯誤。但模板定義很特殊。由template<...>處理的任何東西都意味着編譯器在當時不爲它分配存儲空間,它 一直出於等待狀態直到被一個模板實例告知。在編譯器和鏈接器的某一處,有一機制能去掉模板的多重定義,因此爲了容易使用,幾乎老是在頭文件中放置所有的模 板聲明和定義。

爲何C++編譯器不能支持對模板的分離式編譯 
劉未鵬(pongba) /文

首先,C++標準中提到,一個編譯單元[translation unit]是指一個.cpp文件以及它所include的全部.h文件,.h文件裏的代碼將會被擴展到包含它的.cpp文件裏,而後編譯器編譯該.cpp 文件爲一個.obj文件,後者擁有PE[Portable Executable,即windows可執行文件]文件格式,而且自己包含的就已是二進制碼,可是,不必定可以執行,由於並不保證其中必定有main 函數。當編譯器將一個工程裏的全部.cpp文件以分離的方式編譯完畢後,再由鏈接器(linker)進行鏈接成爲一個.exe文件。 
舉個例子: 
//---------------test.h-------------------// 
void f();//這裏聲明一個函數f 
//---------------test.cpp--------------// 
#i nclude」test.h」 
void f() 

…//do something 
} //這裏實現出test.h中聲明的f函數 
//---------------main.cpp--------------// 
#i nclude」test.h」 
int main() 

f(); //調用f,f具備外部鏈接類型 

在 這個例子中,test. cpp和main.cpp各被編譯成爲不一樣的.obj文件[姑且命名爲test.obj和main.obj],在main.cpp中,調用了f函數,然而 當編譯器編譯main.cpp時,它所僅僅知道的只是main.cpp中所包含的test.h文件中的一個關於void f();的聲明,因此,編譯器將這裏的f看做外部鏈接類型,即認爲它的函數實現代碼在另外一個.obj文件中,本例也就是test.obj,也就是 說,main.obj中實際沒有關於f函數的哪怕一行二進制代碼,而這些代碼實際存在於test.cpp所編譯成的test.obj中。在 main.obj中對f的調用只會生成一行call指令,像這樣: 
call f [C++中這個名字固然是通過mangling[處理]過的] 
在 編譯時,這個call指令顯然是錯誤的,由於main.obj中並沒有一行f的實現代碼。那怎麼辦呢?這就是鏈接器的任務,鏈接器負責在其它的.obj中 [本例爲test.obj]尋找f的實現代碼,找到之後將call f這個指令的調用地址換成實際的f的函數進入點地址。須要注意的是:鏈接器實際上將工程裏的.obj「鏈接」成了一個.exe文件,而它最關鍵的任務就是 上面說的,尋找一個外部鏈接符號在另外一個.obj中的地址,而後替換原來的「虛假」地址。 
這個過程若是說的更深刻就是: 
call f這行指令其實並非這樣的,它其實是所謂的stub,也就是一個 
jmp 0x23423[這個地址多是任意的,然而關鍵是這個地址上有一行指令來進行真正的call f動做。也就是說,這個.obj文件裏面全部對f的調用都jmp向同一個地址,在後者那兒才真正」call」f。這樣作的好處就是鏈接器修改地址時只要對 後者的call XXX地址做改動就好了。可是,鏈接器是如何找到f的實際地址的呢[在本例中這處於test.obj中],由於.obj於.exe的格式都是同樣的,在這 樣的文件中有一個符號導入表和符號導出表[import table和export table]其中將全部符號和它們的地址關聯起來。這樣鏈接器只要在test.obj的符號導出表中尋找符號f[固然C++對f做了mangling]的 地址就好了,而後做一些偏移量處理後[由於是將兩個.obj文件合併,固然地址會有必定的偏移,這個鏈接器清楚]寫入main.obj中的符號導入表中f 所佔有的那一項。 
這就是大概的過程。其中關鍵就是: 
編譯main.cpp時,編譯器不知道f的實現,全部當碰到對它的調用時只是給出一個指示,指示鏈接器應該爲它尋找f的實現體。這也就是說main.obj中沒有關於f的任何一行二進制代碼。 
編譯test.cpp時,編譯器找到了f的實現。因而乎f的實現[二進制代碼]出如今test.obj裏。 
鏈接時,鏈接器在test.obj中找到f的實現代碼[二進制]的地址[經過符號導出表]。而後將main.obj中懸而未決的call XXX地址改爲f實際的地址。 
完成。

然而,對於模板,你知道,模板函數的代碼其實並不能直接編譯成二進制代碼,其中要有一個「具現化」的過程。舉個例子: 
//----------main.cpp------// 
template<class T> 
void f(T t) 
{} 
int main() 

…//do something 
f(10); //call f<int> 編譯器在這裏決定給f一個f<int>的具現體 
…//do other thing 

也就是說,若是你在main.cpp文件中沒有調用過f,f也就得不到具現,從而main.obj中也就沒有關於f的任意一行二進制代碼!!若是你這樣調用了: 
f(10); //f<int>得以具現化出來 
f(10.0); //f<double>得以具現化出來 
這樣main.obj中也就有了f<int>,f<double>兩個函數的二進制代碼段。以此類推。 
然而具現化要求編譯器知道模板的定義,不是嗎? 
看下面的例子:[將模板和它的實現分離] 
//-------------test.h----------------// 
template<class T> 
class A 

public: 
void f(); //這裏只是個聲明 
}; 
//---------------test.cpp-------------// 
#i nclude」test.h」 
template<class T> 
void A<T>::f() //模板的實現,但注意:不是具現 

…//do something 

//---------------main.cpp---------------// 
#i nclude」test.h」 
int main() 

A<int> a; 
a. f(); //編譯器在這裏並不知道A<int>::f的定義,由於它不在test.h裏面 
//因而編譯器只好寄但願於鏈接器,但願它可以在其餘.obj裏面找到 
//A<int>::f的實現體,在本例中就是test.obj,然而,後者中真有A<int>::f的 
//二進制代碼嗎?NO!!!由於C++標準明確表示,當一個模板不被用到的時 
//侯它就不應被具現出來,test.cpp中用到了A<int>::f了嗎?沒有!!因此實 
//際上test.cpp編譯出來的test.obj文件中關於A::f的一行二進制代碼也沒有 
//因而鏈接器就傻眼了,只好給出一個鏈接錯誤 
// 可是,若是在test.cpp中寫一個函數,其中調用A<int>::f,則編譯器會將其//具現出來,由於在這個點上[test.cpp 中],編譯器知道模板的定義,因此能//夠具現化,因而,test.obj的符號導出表中就有了A<int>::f這個符號的地 
//址,因而鏈接器就可以完成任務。 
}

關鍵是:在分離式編譯的環境下,編譯器編譯某一個.cpp文件時並不知道另外一個.cpp文件的存在,也不會去查找[當遇到未決符號時它會寄但願於連 接器]。這種模式在沒有模板的狀況下運行良好,但遇到模板時就傻眼了,由於模板僅在須要的時候纔會具現化出來,因此,當編譯器只看到模板的聲明時,它不能 具現化該模板,只能建立一個具備外部鏈接的符號並期待鏈接器可以將符號的地址決議出來。然而當實現該模板的.cpp文件中沒有用到模板的具現體時,編譯器 懶得去具現,因此,整個工程的.obj中就找不到一行模板具現體的二進制代碼,因而鏈接器也黔

/////////////////////////////////
http://dev.csdn.net/develop/article/19/19587.shtm 
 C++模板代碼的組織方式 ——包含模式(Inclusion Model)     選擇自 sam1111 的 Blog  
關鍵字   Template Inclusion Model 
出處   C++ Template: The Complete Guide


說明:本文譯自《C++ Template: The Complete Guide》一書的第6章中的部份內容。最近看到C++論壇上常有關於模板的包含模式的帖子,聯想到本身初學模板時,也爲相似的問題困惑過,所以翻譯此文,但願對初學者有所幫助。

模板代碼有幾種不一樣的組織方式,本文介紹其中最流行的一種方式:包含模式。

連接錯誤

大多數C/C++程序員向下面這樣組織他們的非模板代碼:

         ·類和其餘類型所有放在頭文件中,這些頭文件具備.hpp(或者.H, .h, .hh, .hxx)擴展名。

         ·對於全局變量和(非內聯)函數,只有聲明放在頭文件中,而定義放在點C文件中,這些文件具備.cpp(或者.C, .c, .cc, .cxx)擴展名。

這種組織方式工做的很好:它使得在編程時能夠方便地訪問所需的類型定義,而且避免了來自連接器的「變量或函數重複定義」的錯誤。

因爲以上組織方式約定的影響,模板編程新手每每會犯一個一樣的錯誤。下面這一小段程序反映了這種錯誤。就像對待「普通代碼」那樣,咱們在頭文件中定義模板:

// basics/myfirst.hpp 

#ifndef MYFIRST_HPP 
#define MYFIRST_HPP 

// declaration of template

template <typename T>

void print_typeof (T const&);

#endif // MYFIRST_HPP

print_typeof()聲明瞭一個簡單的輔助函數用來打印一些類型信息。函數的定義放在點C文件中:

// basics/myfirst.cpp

#i nclude <iostream>

#i nclude <typeinfo>

#i nclude "myfirst.hpp"

// implementation/definition of template

template <typename T> 
void print_typeof (T const& x) 
{

    std::cout << typeid(x).name() << std::endl;

}

這個例子使用typeid操做符來打印一個字符串,這個字符串描述了傳入的參數的類型信息。

最後,咱們在另一個點C文件中使用咱們的模板,在這個文件中模板聲明被#i nclude:

// basics/myfirstmain.cpp 

#i nclude "myfirst.hpp" 

// use of the template

int main() 
{

    double ice = 3.0; 
    print_typeof(ice);  // call function template for type double

}


大部分C++編譯器(Compiler)極可能會接受這個程序,沒有任何問題,可是連接器(Linker)大概會報告一個錯誤,指出缺乏函數print_typeof()的定義。

這個錯誤的緣由在於,模板函數print_typeof()的定義尚未被具現化(instantiate)。爲了具現化一個模板,編譯器必須知道 哪個定義應該被具現化,以及使用什麼樣的模板參數來具現化。不幸的是,在前面的例子中,這兩組信息存在於分開編譯的不一樣文件中。所以,當咱們的編譯器看 到對print_typeof()的調用,可是沒有看到此函數爲double類型具現化的定義時,它只是假設這樣的定義在別處提供,而且建立一個那個定義 的引用(連接器使用此引用解析)。另外一方面,當編譯器處理myfirst.cpp時,該文件並無任何指示代表它必須爲它所包含的特殊參數具現化模板定 義。

頭文件中的模板

解決上面這個問題的通用解法是,採用與咱們使用宏或者內聯函數相同的方法:咱們將模板的定義包含進聲明模板的頭文件中。對於咱們的例子,咱們能夠通 過將#i nclude "myfirst.cpp"添加到myfirst.hpp文件尾部,或者在每個使用咱們的模板的點C文件中包含myfirst.cpp文件,來達到目的。固然,還有第三種方法,就是刪掉myfirst.cpp文件,並重寫myfirst.hpp文件,使它包含全部的模板聲明與定義:


// basics/myfirst2.hpp

#ifndef MYFIRST_HPP 
#define MYFIRST_HPP 

#i nclude <iostream> 
#i nclude <typeinfo>

// declaration of template 
template <typename T> 
void print_typeof (T const&); 

// implementation/definition of template 
template <typename T> 
void print_typeof (T const& x) 
{

    std::cout << typeid(x).name() << std::endl;

}

#endif // MYFIRST_HPP

這種組織模板代碼的方式就稱做包含模式。通過這樣的調整,你會發現咱們的程序已經可以正確編譯、連接、執行了。

從這個方法中咱們能夠獲得一些觀察結果。最值得注意的一點是,這個方法在至關程度上增長了包含myfirst.hpp的開銷。在這個例子中,這種開 銷並非由模板定義自身的尺寸引發的,而是由這樣一個事實引發的,即咱們必須包含咱們的模板用到的頭文件,在這個例子中 是<iostream>和<typeinfo>。你會發現這最終致使了成千上萬行的代碼,由於諸 如<iostream>這樣的頭文件也包含了和咱們相似的模板定義。

這在實踐中確實是一個問題,由於它增長了編譯器在編譯一個實際程序時所需的時間。咱們所以會在之後的章節中驗證其餘一些可能的方法來解決這個問題。但不管如何,現實世界中的程序花一小時來編譯連接已是快的了(咱們曾經遇到過花費數天時間來從源碼編譯的程序)。

拋開編譯時間不談,咱們強烈建議若是可能儘可能按照包含模式組織模板代碼。

另外一個觀察結果是,非內聯模板函數與內聯函數和宏的最重要的不一樣在於:它並不會在調用端展開。相反,當模板函數被具現化時,會產生此函數的一個新的 拷貝。因爲這是一個自動的過程,編譯器也許會在不一樣的文件中產生兩個相同的拷貝,從而引發連接器報告一個錯誤。理論上,咱們並不關心這一點:這是編譯器設 計者應當關心的事情。實際上,大多數時候一切都運轉正常,咱們根本就不用處理這種情況。然而,對於那些須要建立本身的庫的大型項目,這個問題偶爾會顯現出 來。

最後,須要指出的是,在咱們的例子中,應用於普通模板函數的方法一樣適用於模板類的成員函數和靜態數據成員,以及模板成員函數。

相關文章
相關標籤/搜索