1. 程式人生 > >模板和泛型程式設計--模板定義--第十六章 --c++ primer

模板和泛型程式設計--模板定義--第十六章 --c++ primer

第十六章 模板和泛型程式設計
所謂泛型程式設計就是以獨立於任何特定型別的方式編寫程式碼。使用泛型程式
時,我們需要提供具體程式例項所操作的型別或值。第二部分中描述的標準庫的
容器、迭代器和演算法都是泛型程式設計的例子。每種容器(如 vector)都有單一的定
義,但可以定義許多不同種類的 vector,它們的區別在於所包含的元素型別。
模板是泛型程式設計的基礎。使用模板時可以無須瞭解模板的定義。本章將介紹
怎樣定義自己的模板類和模板函式。
泛型程式設計與面向物件程式設計一樣,都依賴於某種形式的多型性。面向物件程式設計
中的多型性在執行時應用於存在繼承關係的類。我們能夠編寫使用這些類的代
碼,忽略基類與派生類之間型別上的差異。只要使用基類的引用或指標,基類類
型或派生類型別的物件就可以使用相同的程式碼。
在泛型程式設計中,我們所編寫的類和函式能夠多型地用於跨越編譯時不相關的
型別。一個類或一個函式可以用來操縱多種型別的物件。標準庫中的容器、迭代
器和演算法是很好的泛型程式設計的例子。標準庫用獨立於型別的方式定義每個容器、
迭代器和演算法,因此幾乎可以在任意型別上使用標準庫的類和函式。例如,雖然
vector 的設計者不可能瞭解應用程式特定的類,但我們能夠定義 Sales_item
物件組成的 vector。
在 C++ 中,模板是泛型程式設計的基礎。模板是建立類或函式的藍圖或公式。
例如,標準庫定義了一個類模板,該模板定義了 vector 的含義,它可以用於產
生任意數量的特定型別的 vector 類,例如,vector 或 vector。
本書第二部分介紹了怎樣使用泛型型別和泛型函式,本章將介紹怎樣自定義模
板。
16.1. 模板定義
假設想要編寫一個函式比較兩個值並指出第一個值是小於、等於還是大於第
二個值。實踐中,我們可能希望定義幾個這樣的函式,每一個可以比較一種給定
型別的值,第一次嘗試可能是定義幾個過載函式:

    // returns 0 if the values are equal, -1 if v1 is smaller, 1 if v2
is smaller
     int compare(const string &v1, const string &v2)
     {
         if (v1 < v2) return -1;
         if (v2 < v1) return 1;
         return 0;
     }
     int compare(const double &v1, const double &v2)

     {
         if
(v1 < v2) return -1; if (v2 < v1) return 1; return 0; }
這些函式幾乎相同,它們之間唯一的區別是形參的型別,每個函式的函式體

是相同的。
每個要比較的型別都需要重複函式的函式體,不僅麻煩而且容易出錯。更重
要的是, 需要事先知道空間可能會比較哪些型別。 如果希望將函式用於未知型別,
這種策略就不起作用了。

16.1.1. 定義函式模板
我們可以不用為每個型別定義一個新函式,而是隻定義一個函式模板
(function template)。函式模板是一個獨立於型別的函式,可作為一種方式,
產生函式的特定型別版本。例如,可以編寫名為 compare 的函式模板,它告訴
編譯器如何為我們想要比較的型別產生特定的 compare 版本。
下面是 compare 的模板版本:

    // implement strcmp-like generic compare function
     // returns 0 if the values are equal, 1 if v1 is larger, -1 if v1
is smaller
     template <typename T>
     int compare(const T &v1, const T &v2)
     {
         if (v1 < v2) return -1;
         if (v2 < v1) return 1;
         return 0;
     }
模板定義以關鍵字 template 開始,後接模板形參表,模板形參表是用尖括

號括住的一個或多個模板形參的列表,形參之間以逗號分隔。

模板形參表不能為空。
模板形參表
模板形參表很像函式形參表,函式形參表定義了特定型別的區域性變數但並不
初始化那些變數,在執行時再提供實參來初始化形參。
同樣,模板形參表示可以在類或函式的定義中使用的型別或值。例如,
compare 函式宣告一個名為 T 的型別形參。在 compare 內部,可以使用名字 T
引用一個型別,T 表示哪個實際型別由編譯器根據所用的函式而確定。
模板形參可以是表示型別的型別形參,也可以是表示常量表達式的非型別形
參。非型別形參跟在型別說明符之後宣告,第 16.1.5 節將進一步介紹非型別形
參。型別形參跟在關鍵字 class 或 typename 之後定義,例如,class T 是名
為 T 的型別形參,在這裡 class 和 typename 沒有區別。

使用函式模板
使用函式模板時,編譯器會推斷哪個(或哪些)模板實參繫結到模板形參。
一旦編譯器確定了實際的模板實參,就稱它例項化了函式模板的一個例項。實質
上,編譯器將確定用什麼型別代替每個型別形參,以及用什麼值代替每個非型別
形參。推匯出實際模板實參後,編譯器使用實參代替相應的模板形參產生編譯該
版本的函式。編譯器承擔了為我們使用的每種型別而編寫函式的單調工作。
對於以下呼叫

     int main ()
     {
         // T is int;
         // compiler instantiates int compare(const int&, const int&)
         cout << compare(1, 0) << endl;
         // T is string;
         // compiler instantiates int compare(const string&, const
string&)
         string s1 = "hi", s2 = "world";
         cout << compare(s1, s2) << endl;
         return 0;
     }
編譯器將例項化 compare 的兩個不同版本,編譯器將用 int 代替 T 建立

第一個版本,並用 string 代替 T 建立第二個版本。

inline 函式模板
函式模板可以用與非模板函式一樣的方式宣告為 inline。說明符放在模板
形參表之後、返回型別之前,不能放在關鍵字 template 之前。

     // ok: inline specifier follows template parameter list
     template <typename T> inline T min(const T&, const T&);
     // error: incorrect placement of inline specifier
     inline template <typename T> T min(const T&, const T&);

Exercises Section 16.1.1
Exercise
16.1:
編寫一個模板返回形參的絕對值。至少用三種不同型別的
值呼叫模板。注意:在第 16.3 節討論編譯器怎樣處理模
板例項化之前,你應該將每個模板定義和該模板的所有使
用放在同一檔案中。
Exercise
16.2:
編寫一個函式模板,接受一個 ostream 引用和一個值,
將該值寫入流。用至少四種不同型別呼叫函式。通過寫至
cout、寫至檔案和寫至 stringstream 來測試你的程式。
Exercise
16.3:
當呼叫兩個 string 物件的 compare 時,傳遞用字串
字面值初始化的兩個 string 物件。如果編寫以下程式碼會
發生什麼?
compare (“hi”, “world”);

16.1.2. 定義類模板
就像可以定義函式模板一樣,也可以定義類模板。

為了舉例說明類模板,我們將為標準庫 queue 類(第 9.7 節)

實現一個自己的版本。使用者程式應使用標準的 queue 類,而不
是我們這裡定義的這個 Queue 類。
我們自定義的 Queue 類必須能夠支援不同型別的物件, 所以將它定義為類模
板。Queue 類將支援的操作是標準 queue 類介面的子集:
• push 操作,在隊尾增加一項
• pop 操作,從隊頭刪除一項
• front 操作,返回隊頭元素的引用
• empty 操作,指出佇列中是否有元素
第 16.4 節將介紹怎樣實現 Queue 類,這裡先定義它的介面:

     template <class Type> class Queue {
     public:
         Queue ();                // default constructor
         Type &front ();          // return element from head of Queue
         const Type &front () const;
         void push (const Type &); // add element to back of Queue
         void pop();              // remove element from head of Queue
         bool empty() const;      // true if no elements in the Queue
     private:
         // ...
     };
類模板也是模板,因此必須以關鍵字 template 開頭,後接模板形參表。

Queue 模板接受一個名為 Type 的模板型別形參。
除了模板形參表外,類模板的定義看起來與任意其他類問相似。類模板可以
定義資料成員、函式成員和型別成員,也可以使用訪問標號控制對成員的訪問,
還可以定義建構函式和解構函式等等。在類和類成員的定義中,可以使用模板形
參作為型別或值的佔位符,在使用類時再提供那些型別或值。
例如,Queue 模板有一個模板型別形參,可以在任何可以使用型別名字的地
方使用該形參。在這個模板定義中,用 Type 指定過載 front 操作的返回型別
以及作為 push 操作的形參型別。
使用類模板
與呼叫函式模板形成對比,使用類模板時,必須為模板形參顯式指定實參:

     Queue<int> qi;                 // Queue that holds ints
     Queue< vector<double> > qc;    // Queue that holds vectors of
doubles
     Queue<string> qs;              // Queue that holds strings

編譯器使用實參來例項化這個類的特定型別版本。實質上,編譯器用使用者提
供的實際特定型別代替 Type,重新編寫 Queue 類。在這個例子中,編譯器將實
例化三個 Queue 類:第一個用 int 代替 Type,第二個用 vector 代
替 Type,第三個用 string 代替 Type。
Exercises Section 16.1.2
Exercise
16.4:
什麼是函式模板?什麼是類模板?
Exercise
16.5:
定義一個函式模板,返回兩個值中較大的一個。
Exercise
16.6:
類似於我們的 queue 簡化版本,編寫一個名為 List 的
類模板,作為標準 list 類的簡化版本。

16.1.3. 模板形參
像函式形參一樣,程式設計師為模板形參選擇的名字沒有本質含義。在我們的例
子中,將 compare 的模板型別形參命名為 T,但也可以將它命名為任意名字:

     // equivalent template definition
     template <class Glorp>
     int compare(const Glorp &v1, const Glorp &v2)
     {
         if (v1 < v2) return -1;
         if (v2 < v1) return 1;
         return 0;
     }

該程式碼定義的 compare 模板與前面一樣。
可以給模板形參賦予的唯一含義是區別形參是型別形參還是非型別形參。如
果是型別形參,我們就知道該形參表示未知型別,如果是非型別形參,我們就知
道它是一個未知值。
如果希望使用模板形參所表示的型別或值,可以使用與對應模板形參相同的
名字。例如,compare 函式中所有的 Glorp 引用將在該函式被例項化時確定為
同一型別。

模板形參作用域
模板形參的名字可以在宣告為模板形參之後直到模板宣告或定義的末尾處
使用。
模板形參遵循常規名字遮蔽規則。與全域性作用域中宣告的物件、函式或型別
同名的模板形參會遮蔽全域性名字:

     typedef double T;
     template <class T> T calc(const T &a, const T &b)
     {
          // tmp has the type of the template parameter T
          // not that of the global typedef
          T tmp = a;
          // ...
          return tmp;
     }
將 T 定義為 double 的全域性型別型別名將被名為 T 的型別形參所遮蔽,因

此,tmp 不是 double 型,相反,tmp 的型別是繫結到模板形參的任意型別。
使用模板形參名字的限制
用作模板形參的名字不能在模板內部重用。

     template <class T> T calc(const T &a, const T &b)
     {
         typedef double T; // error: redeclares template parameter T
         T tmp = a;
         // ...
         return tmp;
     }

這一限制還意味著模板形參的名字只能在同一模板形參表中使用一次:

     // error: illegal reuse of template parameter name V
     template <class V, class V> V calc(const V&, const V&) ;

當然, 正如可以重用函式形參名字一樣, 模板形參的名字也能在不同模板中重用:

     // ok: reuses parameter type name across different templates
     template <class T> T calc (const T&, const T&) ;
     template <class T> int compare(const T&, const T&) ;

模板宣告
像其他任意函式或類一樣,對於模板可以只宣告而不定義。宣告必須指出函
數或類是一個模板:

// declares compare but does not define it
     template <class T> int compare(const T&, const T&) ;

同一模板的宣告和定義中,模板形參的名字不必相同。

     // all three uses of calc refer to the same function template
     // forward declarations of the template
     template <class T> T calc(const T&, const T&) ;
     template <class U> U calc(const U&, const U&) ;
     // actual definition of the template
     template <class Type>
     Type calc(const Type& a, const Type& b) { }

每個模板型別形參前面必須帶上關鍵字 class 或 typename,每個非型別形
參前面必須帶上型別名字,省略關鍵字或型別說明符是錯誤的:

     // error: must precede U by either typename or class
     template <typename T, U> T calc (const T&, const U&) ; 

Exercises Section 16.1.3
Exercise
16.7:
解釋下面每個函式模板的定義並指出是否有非法的。改正
所發現的錯誤。

     (a) template <class T, U, typename V> void f1(T,
U, V) ;
     (b) template <class T> T f2(int &T) ;
     (c) inline template <class T> T foo(T, unsigned
int*) ;
     (d) template <class T> f4 (T, T) ;
     (e) typedef char Ctype ;
         template <typename Ctype> Ctype f5(Ctype
a) ;

Exercise
16.8:
如果有,解釋下面哪些宣告是錯誤的說明為什麼。

     (a) template <class Type> Type bar(Type, Type) ;
         template <class Type> Type bar(Type,
Type) ;
     (b) template <class T1, class T2> void bar(T1,
T2) ;
         template <class C1, typename C2> void
bar(C1, C2) ;

Exercise
16.9:
編寫行為類似於標準庫中 find 演算法的模板。你的模板應
接受一個型別形參,該形參指定函式形參(一對迭代器)
的型別。使用你的函式在 vector 和 list
中查詢給定值。

16.1.4. 模板型別形參
型別形參由關鍵字 class 或 typename 後接說明符構成。在模板形參表中,這兩個關鍵字具有相同的含義,都指出後面所接的名字表示一個型別。

模板型別形參可作為型別說明符在模板中的任何地方,與內建型別說明符或類型別說明符的使用方式完全相同。具體而言,它可以用於指定返回型別或函式形參型別,以及在函式體中用於變數宣告或強制型別轉換。

     // ok: same type used for the return type and both parameters
     template <class T> T calc (const T& a, const T& b)
     {
          // ok: tmp will have same type as the parameters & return type
          T tmp = a;
          // ...
          return tmp;
     }

typename 與 class 的區別
在函式模板形參表中,關鍵字 typename 和 class 具有相同含義,可以互換使用,兩個關鍵字都可以在同一模板形參表中使用:

///
     // ok: no distinction between typename and class in template
parameter list
     template <typename T, class U> calc (const T&, const U&);
 ////

使用關鍵字 typename 代替關鍵字 class 指定模板型別形參也許更為直觀,畢竟,可以使用內建型別(非類型別)作為實際的型別形參,而且,typename更清楚地指明後面的名字是一個型別名。但是,關鍵字 typename 是作為標準
C++ 的組成部分加入到 C++ 中的,因此舊的程式更有可能只用關鍵字 class。
在模板定義內部指定型別
除了定義資料成員或函式成員之外,類還可以定義型別成員。例如,標準庫的容器類定義了不同的型別,如 size_type,使我們能夠以獨立於機器的方式使用容器。如果要在函式模板內部使用這樣的型別,必須告訴編譯器我們正在使用的名字指的是一個型別。必須顯式地這樣做,因為編譯器(以及程式的讀者)不能通過檢查得知,由型別形參定義的名字何時是一個型別何時是一個值。例如,考慮下面的函式:

     template <class Parm, class U>
     Parm fcn(Parm* array, U value)
     {
         Parm::size_type * p; // If Parm::size_type is a type, then a
declaration

                              // If Parm::size_type is an object, then
multiplication
     }

“`
我們知道 size_type 必定是繫結到 Parm 的那個型別的成員,但我們不知道 size_type 是一個型別成員的名字還是一個數據成員的名字,預設情況下,編譯器假定這樣的名字指定資料成員,而不是型別。如果希望編譯器將 size_type 當作型別,則必須顯式告訴編譯器這樣做:

     template <class Parm, class U>
     Parm fcn(Parm* array, U value)
     {
         typename Parm::size_type * p; // ok: declares p to be a pointer
     }
通過在成員名前加上關鍵字 typename 作為字首,可以告訴編譯器將成員當作型別。通過編寫 typename parm::size_type,指出繫結到 Parm 的型別的size_type 成員是型別的名字。當然,這一宣告給用例項化 fcn 的型別增加了一個職責:那些型別必須具有名為 size_type 的成員,而且該成員是一個型別。 

如果拿不準是否需要以 typename 指明一個名字是一個型別,那麼指定它是個好主意。在型別之前指定 typename 沒有害處,因此,即使 typename 是不必要的,也沒有關係。
Exercises Section 16.1.4
Exercise
16.10:
宣告為 typename 的型別形參與宣告為 class 的型別形參有區別嗎?區別在哪裡?
Exercise
16.11:
何時必須使用 typename?
Exercise
16.12:
編寫一個函式模板,接受表示未知型別迭代器的一對值,找出在序列中出現得最頻繁的值。
Exercise
16.13:
編寫一個函式,接受一個容器的引用並列印該容器的元素。使用容器的 size_type 和 size 成員控制列印元素
的迴圈。
Exercise
16.14:
重新編寫上題的函式,使用從 begin 和 end 返回的迭代器來控制迴圈。

16.1.5. 非型別模板形參
模板形參不必都是型別。本節將介紹函式模板使用的非型別形參。在介紹了類模板實現的更多內容之後,第 16.4.2 節將介紹類模板的非型別形參。在呼叫函式時非型別形參將用值代替, 值的型別在模板形參表中指定。 例如,下面的函式模板聲明瞭 array_init 是一個含有一個型別模板形參和一個非型別模板形參的函式模板。 函式本身接受一個形參, 該形參是陣列的引用 (第 7.2.4
節):


// initialize elements of an array to zero
template <class T, size_t N> void array_init(T (&parm)[N])
{
for (size_t i = 0; i != N; ++i) {
parm[i] = 0;
}
}

模板非型別形參是模板定義內部的常量值,在需要常量表達式的時候,可使用非型別形參(例如,像這裡所做的一樣)指定陣列的長度。

當呼叫 array_init 時,編譯器從陣列實參計算非型別形參的值:
int x[42];
double y[10];
array_init(x); // instantiates array_init(int(&)[42]
array_init(y); // instantiates array_init(double(&)[10]

編譯器將為 array_init 呼叫中用到的每種陣列例項化一個 array_init版本。對於上面的程式,編譯器將例項化 array_init 的兩個版本:第一個例項的形參繫結到 int[42],另一個例項中的形參繫結到 double[10]。
型別等價性與非型別形參對模板的非型別形參而言,求值結果相同的表示式將認為是等價的。下面的兩個 array_init 呼叫引用的是相同的例項——
array_init 《 int, 4

int x[42];
 const int sz = 40;
 int y[sz + 2];
 array_init(x);  // instantiates array_init(int(&)[42])
 array_init(y);  // equivalent instantiation

Exercises Section 16.1.5
Exercise
16.15:
編寫可以確定陣列長度的函式模板。
Exercise
16.16:
將第 7.2.4 節的 printValues 函式重新編寫為可用於
列印不同長度陣列內容的函式模板。

16.1.6. 編寫泛型程式
編寫模板時,程式碼不可能針對特定型別,但模板程式碼總是要對將使用的型別做一些假設。例如,雖然 compare 函式從技術上說任意型別都是有效的,但實際上,例項化的版本可能是非法的。

產生的程式是否合法,取決於函式中使用的操作以及所用型別支援的操作。compare 函式有三條語句:
if (v1 < v2) return -1; // < on two objects of type T
if (v2 < v1) return 1; // < on two objects of type T
return 0; // return int; not dependent on T

前兩條語句包含隱式依賴於形參型別的程式碼,if 測試對形參使用 < 操作符,直到編譯器看見 compare 呼叫並且 T 繫結到一個實際型別時,才知道形參的型別,使用哪個 < 操作符完全取決於實參型別。如果用不支援 < 操作符的物件呼叫 compare,則該呼叫將是無效的:
Sales_item item1, item2;
// error: no < on Sales_item
cout << compare(item1, item2) << endl;

程式會出錯。Sales_item 型別沒有定義 < 操作符,所以該程式不能編譯。

在函式模板內部完成的操作限制了可用於例項化該函式的型別。程式設計師的責任是,保證用作函式實參的型別實際上支援所用的任意操作,以及保證在模板使用哪些操作的環境中那些操作執行正常。

編寫獨立於型別的程式碼
編寫良好泛型程式碼的技巧超出了本書的範圍,但是,有個一般原則值得注意。

編寫模板程式碼時,對實參型別的要求儘可能少是很有益的。

雖然簡單,但它說明了編寫泛型程式碼的兩個重要原則:
• 模板的形參是 const 引用。

• 函式體中的測試只用 < 比較。
通過將形參設為 const 引用,就可以允許使用不允許複製的型別。大多數型別(包括內建型別和我們已使用過的除 IO 型別之外的所有標準庫的型別)都允許複製。但是,也有不允許複製的類型別。將形參設為 const 引用,保證這種型別可以用於 compare 函式,而且,如果有比較大的物件呼叫 compare,則這個設計還可以使函式執行得更快。一些讀者可能認為使用 < 和 > 操作符兩者進行比較會更加自然:
// expected comparison
if (v1 < v2) return -1;
if (v1 > v2) return 1;
return 0;

但是,將程式碼編寫為
// expected comparison
if (v1 < v2) return -1;
if (v2 < v1) return 1; // equivalent to v1 > v2
return 0;

可以減少對可用於 compare 函式的型別的要求,這些型別必須支援 <,但不必
支援 >。
Exercises Section 16.1.6
Exercise
16.17:
在第 3.3.2 節的“關鍵概念”中,我們注意到,C++ 程式設計師習慣於使用 != 而不用 <,解釋這一習慣的基本原
理。
Exercise
16.18:
本節中我們提到應該慎重地編寫 compare 中的資訊理論以避免要求型別同時具有 < 和 > 操作符,另一方面,往往
假定型別既有 == 又有 !=。解釋為什麼這一看似不一致的處理實際上反映了良好的程式設計風格。

警告:連結時的編譯時錯誤
一般而言,編譯模板時,編譯器可能會在三個階段中標識錯誤:第一階段是編譯模板定義本身時。 在這個階段中編譯器一般不能發現許多錯誤,可以檢測到諸如漏掉分號或變數名拼寫錯誤一類的語法錯誤。第二個錯誤檢測時間是在編譯器見到模板的使用時。在這個階段,編譯器仍沒有很多檢查可做。對於函式模板的呼叫,許多編譯器只檢查實參的數目和型別是否恰當,編譯器可以檢測到實參太多或太少,也可以檢測到假定型別相同的兩個實參是否真地型別相同。對於類模板,編譯器可以檢測提供的模板實參的正確數目。產生錯誤的第三個時間是在例項化的時候,只有在這個時候可以發現型別相關的錯誤。根據編譯器管理例項化的方式(將在第 16.3 節討論),有可能在連結時報告這些錯誤。重要的是,要認識到編譯模板定義的時候,對程式是否有效所知不多。類似地,甚至可能會在已經成功編譯了使用模板的每個檔案之後出現編譯錯誤。只在例項化期間檢測錯誤的情況很少,錯誤檢測可能發生在連結時。