跳轉到內容

C++ 程式設計/模板

來自華夏公益教科書,自由的教科書

模板是一種使程式碼更可重用的方法。簡單的例子包括建立可以儲存任意資料型別的通用資料結構。模板對程式設計師非常有用,尤其是在結合多個 繼承運算子過載 時。 標準模板庫 (STL) 在連線模板的框架內提供了許多有用的函式。

由於模板非常有表現力,因此它們可以用於除泛型程式設計之外的其他用途。一種這樣的用途稱為 模板超程式設計,它是在編譯時而不是執行時預先評估部分程式碼的一種方法。這裡進一步討論僅與模板作為泛型程式設計方法有關。

到目前為止,你應該已經注意到執行相同任務的函式往往看起來很相似。例如,如果你編寫了一個列印 int 的函式,則必須先宣告 int。這樣可以減少程式碼中的錯誤可能性,但是,必須建立函式的不同版本來處理你使用的所有不同資料型別,這會讓人感到有點煩人。例如,你可能希望該函式僅列印輸入變數,無論該變數是什麼型別。為每種可能的輸入型別編寫一個不同的函式(double, char *等)將非常繁瑣。這就是模板的用武之地。

模板解決了與宏相同的一些問題,在編譯時生成“最佳化”的程式碼,但受 C++ 的嚴格型別檢查的約束。

引數化型別,更廣為人知的是模板,允許程式設計師建立一個可以處理多種不同型別的函式。你無需考慮每種資料型別,而只需使用一個任意的引數名稱,編譯器隨後會用你希望函式使用、操作等的不同資料型別替換該名稱。

  • 模板在編譯時使用原始碼進行例項化。
  • 模板是型別安全的。
  • 模板允許使用者定義的專門化。
  • 模板允許非型別引數。
  • 模板使用“延遲結構約束”。
  • 模板支援混合。
模板語法

模板非常易於使用,只需檢視語法即可

 template <class TYPEPARAMETER>

(或等效地,並被某些人更喜歡)

 template <typename TYPEPARAMETER>

函式模板

[編輯 | 編輯原始碼]

有兩種型別的模板。函式模板的行為類似於可以接受多種不同型別引數的函式。例如,標準模板庫包含函式模板max(x, y)它返回xy,以較大者為準。max()可以這樣定義

    template <typename TYPEPARAMETER>
    TYPEPARAMETER max(TYPEPARAMETER x, TYPEPARAMETER y)
    {
        if (x < y)
            return y;
        else
            return x;
    }

此模板可以像函式一樣呼叫

    std::cout << max(3, 7);   // outputs 7

編譯器透過檢查引數來確定這是一個對max(int, int)的呼叫,並例項化函式的版本,其中型別TYPEPARAMETERint.

這無論引數xy是整數、字串還是任何其他型別(對於這些型別,可以說“x < y”)都適用。如果你定義了自己的資料型別,則可以使用運算子過載來定義你型別的 < 的含義,從而允許你使用max()函式。雖然這在孤立的例子中似乎是一個微不足道的優勢,但在 STL 這樣的綜合庫的背景下,它允許程式設計師僅透過為新資料型別定義幾個運算子來獲得廣泛的功能。僅僅定義<允許使用標準的型別sort(), stable_sort()binary_search()演算法;set、堆和關聯陣列等資料結構;等等。

作為反例,標準型別complex沒有定義<運算子,因為 複數 沒有嚴格的順序。因此,如果 xycomplex值,則 max(x, y) 會導致編譯錯誤。同樣,其他依賴於<的模板無法應用於complex資料。不幸的是,編譯器歷史上針對這種型別的錯誤生成了相當晦澀難懂且無益的錯誤訊息。確保某個物件符合 方法協議 可以緩解這個問題。

{TYPEPARAMETER}只是一個任意的 TYPEPARAMETER 名稱,你希望在函式中使用它。一些程式設計師更喜歡只使用T來代替TYPEPARAMETER.

假設你想要建立一個可以處理多個數據型別的交換函式......類似於這樣

 template <class SOMETYPE> 
 void swap (SOMETYPE &x, SOMETYPE &y) 
 { 
   SOMETYPE temp = x; 
   x = y; 
   y = temp; 
 }

你看到的函式與任何其他交換函式非常相似,不同之處在於函式定義之前的模板 <class SOMETYPE> 行以及程式碼中的 SOMETYPE 例項。在通常需要使用你使用的型別名稱或類的任何地方,你現在用你在模板 <class SOMETYPE> 中使用的任意名稱替換它。例如,如果你使用 SUPERDUPERTYPE 而不是 SOMETYPE,程式碼將如下所示

 template <class SUPERDUPERTYPE> 
 void swap (SUPERDUPERTYPE &x, SUPERDUPERTYPE &y) 
 { 
   SUPERDUPERTYPE temp = x; 
   x = y; 
   y = temp; 
 }

如你所見,你可以使用你想要的任何標籤作為模板 TYPEPARAMETER,只要它不是保留字即可。

類模板

[編輯 | 編輯原始碼]

類模板將相同的概念擴充套件到類。類模板通常用於建立通用容器。例如,STL 有一個 連結串列 容器。要建立一個整數連結串列,可以編寫list<int>。字串連結串列表示為list<string>。一個list具有一組與其關聯的標準函式,無論你在方括號之間放置什麼,這些函式都能正常工作。

如果你想要擁有多個模板 TYPEPARAMETER,則語法將是

 template <class SOMETYPE1, class SOMETYPE2, ...>
模板和類

假設你想要建立的不是一個簡單的模板函式,而是希望將模板用於類,以便該類可以處理多個數據型別。你可能已經注意到,某些類能夠接受型別作為引數並根據該型別建立物件的變體(例如 STL 容器類層次結構的類)。這是因為它們被宣告為模板,使用的語法與下面介紹的語法類似

 template <class T> class Foo
 {
 public:
   Foo();
   void some_function();
   T some_other_function();

 private:
   int member_variable;
   T parametrized_variable;
 };

定義模板類的成員函式有點像定義函式模板,除了你需要使用作用域解析運算子來指示這是模板類的成員函式。唯一一個重要且不明顯的細節是需要在類名之後使用包含引數化型別名稱的模板運算子。

以下示例透過定義來自上述示例類的函式來描述所需的語法。

 template <class T> Foo<T>::Foo()
 {
   member_variable = 0;
 }

 template <class T> void Foo<T>::some_function()
 {
   cout << "member_variable = " << member_variable << endl;
 }

 template <class T> T Foo<T>::some_other_function()
 {
   return parametrized_variable;
 }

如你所見,如果你想要宣告一個將返回引數化型別物件的函式,你只需將該引數的名稱用作函式的返回值型別即可。

注意
類模板可以用來避免繼承中虛擬成員函式的開銷。由於類的型別在編譯時已知,因此類模板不需要具有虛擬成員函式的類所需的虛擬指標表。這種區別還允許內聯類模板的函式成員。

優點和缺點

[編輯 | 編輯原始碼]

模板的一些用途,例如max()函式,以前由類似函式的 預處理器 填補。

// a max() macro
#define max(a,b)   ((a) < (b) ? (b) : (a))

宏和模板都在編譯時展開。宏始終內聯展開;模板也可以在編譯器認為合適時內聯展開。因此,類似函式的宏和函式模板都沒有執行時開銷。

但是,模板通常被認為是這些用途的宏的改進。模板是型別安全的。模板避免了在大量使用類似函式的宏的程式碼中發現的一些常見錯誤。也許最重要的是,模板旨在適用於比宏更大的問題。類似函式的宏的定義必須適合一行程式碼。

使用模板有三個主要缺點。首先,許多編譯器歷史上對模板的支援非常糟糕,因此使用模板會使程式碼的可移植性略有降低。其次,幾乎所有編譯器在模板程式碼中檢測到錯誤時都會生成令人困惑且無用的錯誤訊息。這使得模板難以開發。第三,模板的每次使用都可能導致編譯器生成額外的程式碼(模板的例項化),因此不加區分地使用模板會導致 程式碼膨脹,從而導致可執行檔案過大。

模板的另一個主要缺點是,無法用與不同型別或函式呼叫相同方式起作用的 #define(如 max)進行替換。模板已經取代了使用 #define 用於複雜函式,但沒有用於簡單的東西,如 max(a,b)。有關嘗試為 #define max 建立模板的完整討論,請參閱 Scott Meyer 於 1995 年 1 月為C++ Report 撰寫的論文 "Min, Max and More"

使用模板的最大優勢是,一個複雜的演算法可以有一個簡單的介面,然後編譯器使用該介面根據引數的型別選擇正確的實現。例如,搜尋演算法可以利用正在搜尋的容器的屬性。這種技術在整個 C++ 標準庫中都有使用。

連結問題

[編輯 | 編輯原始碼]

在連結一個由多個模組組成的模板程式時,這些模組分散在多個檔案中,經常會遇到一個令人困惑的問題:模組的程式碼無法連結,因為出現了“未解析的引用到(插入模板成員函式名稱)在(...)中”。出錯的函式實現就在那裡,那為什麼它會從目的碼中丟失呢?讓我們停下來思考一下,這怎麼可能呢?

假設您建立了一個名為 Foo 的模板類,並將它的宣告放在 Util.hpp 檔案中,以及一些其他的普通類,比如 Bar。

 template <class T> Foo
 {
 public: 
   Foo();
   T some_function();
   T some_other_function();
   T some_yet_other_function();
   T member;
 };

 class Bar
 {
   Bar();
   void do_something();
 };

現在,為了遵循所有藝術規則,您建立了一個名為 Util.cc 的檔案,在其中放置所有函式定義,無論是模板還是其他。

 #include "Util.hpp"

 template <class T> T Foo<T>::some_function()
 {
  ...
 }

 template <class T> T Foo<T>::some_other_function()
 {
  ...
 }

 template <class T> T Foo<T>::some_yet_other_function()
 {
  ...
 }

最後,

 void Bar::do_something()
 {
   Foo<int> my_foo;
   int x = my_foo.some_function();
   int y = my_foo.some_other_function();
 }

接下來,您編譯模組,沒有錯誤,您很高興。但是假設程式中還有另一個(主)模組,位於 MyProg.cc 中。

 #include "Util.hpp"	// imports our utility classes' declarations, including the template

 int main()
 {
   Foo<int> main_foo;
   int z = main_foo.some_yet_other_function();
   return 0;
 }

它也乾淨地編譯到目的碼。但是,當您嘗試將這兩個模組連結在一起時,您會收到一個錯誤,提示在 MyProg.cc 中有一個未定義的引用到 Foo<int>::some_yet_other function()。您已經正確地定義了模板成員函式,那麼問題是什麼呢?

您還記得,模板是在編譯時例項化的。這有助於避免程式碼膨脹,因為如果為所有可能的型別及其引數生成所有模板類和函式的變體,將會導致程式碼膨脹。因此,當編譯器處理 Util.cc 程式碼時,它看到 Foo 類的唯一變體是 Foo<int>,並且唯一需要的函式是

 int Foo<int>::some_function();
 int Foo<int>::some_other_function();

Util.cc 中的程式碼不需要任何其他變體的 Foo 或其方法,所以編譯器沒有生成除了這些程式碼之外的其他程式碼。目的碼中沒有 some_yet_other_function() 的實現,就像沒有以下實現一樣:

 double Foo<double>::some_function();

 string Foo<string>::some_function();

MyProg.cc 程式碼編譯沒有錯誤,因為 Foo 中使用的成員函式在 Util.hpp 標頭檔案中正確宣告,並且預計在連結時它會可用。但它沒有,因此出現了錯誤,如果你是模板新手,你可能會開始在程式碼中尋找錯誤,而諷刺的是,你的程式碼是完全正確的。

解決方案在一定程度上取決於編譯器。對於 GNU 編譯器,嘗試使用 -frepo 標誌進行實驗,並閱讀 “info gcc”(節點 "模板例項化":"模板在哪裡?")中關於模板的部分可能會有所啟發。據說在 Borland 中,連結器選項中有一個選項,可以為這類問題啟用“智慧”模板。

您還可以嘗試另一種方法,稱為顯式例項化。您要做的是在包含模板的模組中建立一些虛擬程式碼,這些程式碼會建立模板類的所有變體,並呼叫所有已知在其他地方需要的成員函式變體。顯然,這要求您瞭解整個程式碼中需要哪些變體。在我們簡單的示例中,這將是這樣的

1. 在 Util.hpp 中新增以下類宣告

 class Instantiations
 {
 private:
   void Instantiate();
 };

2. 在 Util.cc 中新增以下成員函式定義

 void Instantiations::Instantiate()
 {
   Foo<int> my_foo;
   my_foo.some_yet_other_function();
   // other explicit instantiations may follow
 }

當然,您永遠不需要實際例項化 Instantiations 類,或呼叫其任何方法。它們的存在本身就會讓編譯器生成所有需要的模板變體。現在,目的碼將能夠連結,不會出現問題。

還有一種解決方案,雖然不優雅,但也行之有效。將所有模板函式的定義程式碼移到 Util.hpp 標頭檔案中。這樣做不太好,因為標頭檔案用於宣告,而實現應該在其他地方定義,但這在這種情況中可以解決問題。在編譯 MyProg.cc(以及包含 Util.hpp 的任何其他模組)程式碼時,編譯器會生成所有需要的模板變體,因為定義可以直接使用。

華夏公益教科書