C++中的模板(template)

1. 簡介算法

模板是C++在90年代引進的一個新概念,本來是爲了對容器類(container classes)的支持[1],可是如今模板產生的效果已經遠非當初所能想象。編程

簡單的講,模板就是一種參數化(parameterized)的類或函數,也就是類的形態(成員、方法、佈局等)或者函數的形態(參數、返回值等)能夠被參數改變。更加神奇的是這裏所說的參數,不光是咱們傳統函數中所說的數值形式的參數,還能夠是一種類型(實際上稍微有一些瞭解的人,更多的會注意到使用類型做爲參數,而每每忽略使用數值做爲參數的狀況)。設計模式

舉個經常使用的例子來解釋也許模板就從你腦殼裏的一個模糊的概念變成活生生的代碼了:安全

在C語言中,若是咱們要比較兩個數的大小,經常會定義兩個宏:函數

#define min(a,b) ((a)>(b)?(b):(a))
#define max(a,b) ((a)>(b)?(a):(b))佈局

這樣你就能夠在代碼中:this

return min(10, 4);.net

或者:設計

return min(5.3, 18.6);對象

這兩個宏很是好用,可是在C++中,它們並不像在C中那樣受歡迎。宏由於沒有類型檢查以及天生的不安全(例如若是代碼寫爲min(a++, b--);則顯然結果非你所願),在C++中被inline函數替代。可是隨着你將min/max改成函數,你馬上就會發現這個函數的侷限性 —— 它不能處理你指定的類型之外的其它類型。例如你的min()聲明爲:

int min(int a, int b);

則它顯然不能處理float類型的參數,可是原來的宏卻能夠很好的工做!你隨後大概會想到函數重載,經過重載不一樣類型的min()函數,你仍然可使大部分代碼正常工做。實際上,C++對於這類能夠抽象的算法,提供了更好的辦法,就是模板:

template <class T> const T & min(const T & t1, const T & t2) {
    return t1>t2?t2:t1;
}

這是一個模板函數的例子。在有了模板以後,你就又自由了,能夠像原來在C語言中使用你的min宏同樣來使用這個模板,例如:

return min(10,4);

也能夠:

return min(5.3, 18.6)

你發現了麼?你得到了一個類型安全的、而又能夠支持任意類型的min函數,它是否比min宏好呢?

固然上面這個例子只涉及了模板的一個方面,模板的做用遠不僅是用來替代宏。實際上,模板是泛化編程(Generic Programming)的基礎。所謂的泛化編程,就是對抽象的算法的編程,泛化是指能夠普遍的適用於不一樣的數據類型。例如咱們上面提到的min算法。

2. 語法

你千萬不要覺得我真的要講模板的語法,那太難爲我了,我只是要說一下如何聲明一個模板,如何定義一個模板以及常見的語法方面的問題。

template<> 是模板的標誌,在<>中,是模板的參數部分。參數能夠是類型,也能夠是數值。例如:

template<class T, T t>
class Temp{
public:
    ...
    void print() { cout << t << endl; }
private:
    T t_;
};

在這個聲明中,第一個參數是一個類型,第二個參數是一個數值。這裏的數值,必須是一個常量。例如針對上面的聲明:

Temp<int, 10> temp; // 合法

int i = 10;
Temp<int, i> temp; // 不合法

const int j = 10;
Temp<int, j> temp; // 合法

參數也能夠有默認值:

template<class T, class C=char> ...

默認值的規則與函數的默認值同樣,若是一個參數有默認值,則其後的每一個參數都必須有默認值。

參數的名字在整個模板的做用域內有效,類型參數能夠做爲做用域內變量的類型(例如上例中的T t_),數值型參數能夠參與計算,就象使用一個普一般數同樣(例如上例中的cout << t << endl)。

模板有個值得注意的地方,就是它的聲明方式。之前我一直認爲模板的方法所有都是隱含爲inline的,即便你沒有將其聲明爲inline並將函數體放到了類聲明之外。這是模板的聲明方式給個人錯覺,實際上並不是如此。咱們先來看看它的聲明,一個做爲接口出如今頭文件中的模板類,其全部方法也都必須與類聲明出如今一塊兒。用通俗的話來講,就是模板類的函數體也必須出如今頭文件中(固然若是這個模板只被一個C++程序文件使用,它固然也能夠放在.cc中,但一樣要求類聲明與函數體必須出如今一塊兒)。這種要求與inline的要求同樣,所以我一度認爲它們隱含都是inline的。可是在Thinking In C++[2]中,明確的提到了模板的non-inline function,就讓我不得不改變本身的想法了。看來正確的理解應該是:與普通類同樣,聲明爲inline的,或者雖然沒有聲明爲inline可是函數體在類聲明中的纔是inline函數。

澄清了inline的問題候,咱們再回頭來看那些咱們寫的包含了模板類的醜陋的頭文件,因爲上面提到的語法要求,頭文件中除了類接口以外,處處充斥着實現代碼,對用戶來講,十分的不可讀。爲了能像傳統頭文件同樣,讓用戶儘可能只看到接口,而不用看到實現方法,通常會將全部的方法實現部分,放在一個後綴爲.i或者.inl的文件中,而後在模板類的頭文件中包含這個.i或者.inl文件。例如:

// start of temp.h
template<class T> class Temp{
public:
    void print();
};

 #include "temp.inl"
// end of temp.h

// start of temp.inl
template<class T> void Temp<T>::print() {
    ...
}
// end of temp.inl

經過這樣的變通,即知足了語法的要求,也讓頭文件更加易讀。模板函數也是同樣。

普通的類中,也能夠有模板方法,例如:

class A{
public:
    template<class T> void print(const T& t) { ...}
    void dummy();
};

對於模板方法的要求與模板類的方法同樣,也須要與類聲明出如今一塊兒。而這個類的其它方法,例如dummy(),則沒有這樣的要求。

3. 使用技巧

知道了上面所說的簡單語法後,基本上就能夠寫出本身的模板了。可是在使用的時候仍是有些技巧。

3.1 語法檢查

對模板的語法檢查有一部分被延遲到使用時刻(類被定義[3],或者函數被調用),而不是像普通的類或者函數在被編譯器讀到的時候就會進行語法檢查。所以,若是一個模板沒有被使用,則即便它包含了語法的錯誤,也會被編譯器忽略,這是語法檢查問題的第一個方面,這不常遇到,由於你寫了一個模板就是爲了使用它的,通常不會放在那裏不用。與語法檢查相關的另外一個問題是你能夠在模板中作一些假設。例如:

template<class T> class Temp{
public:
    Temp(const T & t): t_(t) {}
    void print() { t.print();}
private:
    T t_;
};

在這個模板中,我假設了T這個類型是一個類,而且有一個print()方法(t.print())。咱們在簡介中的min模板中其實也做了一樣的假設,即假設T重載了'>'操做符。

由於語法檢查被延遲,編譯器看到這個模板的時候,並不去關心T這個類型是否有print()方法,這些假設在模板被使用的時候才被編譯器檢查。只要定義中給出的類型知足假設,就能夠經過編譯。

之因此說「有一部分」語法檢查被延遲,是由於有些基本的語法仍是被編譯器當即檢查的。只有那些與模板參數相關的檢查纔會被推遲。若是你沒有寫class結束後的分號,編譯器不會放過你的。

3.2 繼承

模板類能夠與普通的類同樣有基類,也一樣能夠有派生類。它的基類和派生類既能夠是模板類,也能夠不是模板類。全部與繼承相關的特色模板類也都具有。但仍然有一些值得注意的地方。

假設有以下類關係:

template<class T> class A{ ... };
 |
+-- A<int> aint;
 |
+-- A<double> adouble;

則aint和adouble並不是A的派生類,甚至能夠說根本不存在A這個類,只有A<int>和A<doubl>這兩個類。這兩個類沒有共同的基類,所以不能經過類A來實現多態。若是但願對這兩個類實現多態,正確的類層次應該是:

class Abase {...};

template<class T> class A: public Abase {...};
 |
+-- A<int> aint;
 |
+-- A<double> adouble;

也就是說,在模板類之上增長一個抽象的基類,注意,這個抽象基類是一個普通類,而非模板。

再來看下面的類關係:

template<int i> class A{...};
 |
+-- A<10> a10;
 |
+-- A<5> a5;

在這個狀況下,模板參數是一個數值,而不是一個類型。儘管如此,a10和a5仍然沒有共同基類。這與用類型做模板參數是同樣的。

3.3 靜態成員

與上面例子相似:

template<class T> class A{ static char a_; };
 |
+-- A<int> aint1, aint2;
 |
+-- A<double> adouble1, adouble2;

這裏模板A中增長了一個靜態成員,那麼要注意的是,對於aint1和adouble1,它們並無一個共同的靜態成員。而aint1與aint2有一個共同的靜態成員(對adouble1和adouble2也同樣)。

這個問題實際上與繼承裏面講到的問題是一回事,關鍵要認識到aint與adouble分別是兩個不一樣類的實例,而不是一個類的兩個實例。認識到這一點後,不少相似問題均可以想通了。

3.4 模板類的運用

模板與類繼承均可以讓代碼重用,都是對具體問題的抽象過程。可是它們抽象的側重點不一樣,模板側重於對於算法的抽象,也就是說若是你在解決一個問題的時候,須要固定的step1 step2...,那麼大概就能夠抽象爲模板。而若是一個問題域中有不少相同的操做,可是這些操做並不能組成一個固定的序列,大概就能夠用類繼承來解決問題。以個人水平還不足以在這麼高的層次來清楚的解釋它們的不一樣,這段話僅供參考吧。

模板類的運用方式,更多狀況是直接使用,而不是做爲基類。例如人們在使用STL提供的模板時,一般直接使用,而不須要從模板庫中提供的模板再派生本身的類。這不是絕對的,我以爲這也是模板與類繼承之間的以點兒區別,模板雖然也是抽象的東西,可是它每每不須要經過派生來具體化。

在設計模式[4]中,提到了一個模板方法模式,這個模式的核心就是對算法的抽象,也就是對固定操做序列的抽象。雖然不必定要用C++的模板來實現,可是它反映的思想是與C++模板一致的。

4. 參考資料

[1] 深度C++對象模型,Stanley B.Lippman, 侯捷譯

[2] Thinking In C++ 2nd Edition Volumn 1, Bruce Eckel

[3] 定義-- 英文爲definition,意思是"Make this variable here",參見[2] p93

[4] Design Patterns - Elements of Reusable Object-Oriented Software GOF

相關文章
相關標籤/搜索