effective C++筆記--模板與泛型程式設計(一)
文章目錄
瞭解隱式介面和編譯器多型
. 面向物件程式設計世界總是以顯式介面和執行期多型解決問題。比如一個函式中有一個引數是一個類的指標或引用,由於該引數的型別確定,所以它必定支援這一類的介面,可以在原始碼中找到相關的介面(比如標頭檔案中),我們稱此為一個顯示介面;假如這個類的某些成員函式時virtual的,那麼該引數的呼叫將顯現出執行期多型,也就是根據該引數的動態型別來決定究竟呼叫哪個函式。
template及泛型程式設計的世界,與面向物件有根本上的不同。在此世界中顯示介面和執行期多型仍然存在,但重要性降低。反倒是隱式介面和編譯期多型移到前面。在函式模板中會發生什麼事呢:
template<typename T>
void doProcessing(T& w){
if(w.size() > 10 && w != someNastyWidget){
T temp(w);
temp.normalize();
temp.swap(w);
}
}
. w必須支援哪種介面,是由template中執行與w身上的操作來決定的,比如上面的程式碼中w的型別T必須支援size、normalize和swap成員函式、copy建構函式、不等比較。這一組表示式便是T必須支援的一組隱式介面;凡涉及w的任何函式呼叫,例如operator>和operator!=,有可能造成template具現化,使這些呼叫得以成功,這樣的行為發生在編譯期,這就是所謂的編譯器多型。
執行期多型和編譯器多型之間的差異就像是“哪一個過載函式應該被呼叫”和"哪一個virtual函式該被繫結"之間的差異。顯式介面和隱式介面的差異就比較新穎了。
通常顯式介面由函式的簽名式(函式名稱、引數型別、返回型別)組成;隱式介面就完全不同了,它並不基於函式簽名式,而是由有效表示式組成,表示式可能看起來很複雜,但是他們要求的約束條件一般而言相當直接和明確,例如:
if(w.size() > 10 && w != someNastyWidget)…
if語句必須是個布林表示式,所以無論w是什麼型別,無論括號類的內容將導致什麼,它都必須與bool相容。
在template中使用不支援template所要求的隱式介面的物件,將通不過編譯。
瞭解typename的雙重意義
. 首先說明一下在template宣告式中,class和typename沒有什麼不同,不過typename還有特別的用處,那就是指定這個名稱是個型別,比如有一下函式:
//作用是輸出容器中的第二個元素 template<typename C> void f(const C& container){ if(container.size() > 2){ C::const_iterator iter(container.begin()); ++iter; int value = *iter; std::cout<<value; } }
. 其中變數iter的型別實際是什麼取決於template引數C,這類名稱被叫做從屬名稱,如果從屬名稱在class中呈巢狀狀,可稱之為巢狀從屬名稱,比如C::iterator就是這樣一個名稱;另一個變數value型別是int,其不依賴與template引數,這類名稱稱為非從屬名稱。
巢狀從屬名稱可能導致解析困難,比如將上面的程式碼前部分改寫為:
template<typename C>
void f(const C& container){
C::const_iterator* x;
...
}
. 看起來好像是在宣告一個指標,指向C::const_iterator,但是你這麼想是因為你已經知道C::const_iterator是個型別,如果它不是呢?比如C::const_iterator是一個static成員變數,x是一個全域性變數,這句話是不是就變成兩者相乘的意思了。所以要告訴編譯器它是一個型別的辦法就是在緊鄰它之前加上關鍵字typename:
template<typename C>
void f(const C& container){
if(container.size() > 2){
typename C::const_iterator iter(container.begin());
...
}
}
. “typename必須作為巢狀從屬型別名稱的字首詞”這一規則的例外是,typename不可以出現在繼承列表和初始化列表裡。
. 在寫一個程式設計時常見的例子:
template<typename T>
void f(T it){
typename std::iterator_traits<T>::value_type temp(*it);
}
. iterator_traits接受一個原始指標,value_type表示這個指標所指的型別,假如T是vector<int>::iterator的話,temp的型別就是int的。所以前面那麼長一串是型別,需要typename來指出。另外這麼長的一句話多寫幾遍應該會很難受,所以常常將它與typedef一起使用:
template<typename T>
void f(T it){
typedef typename std::iterator_traits<T>::value_type value_type;
value_type temp(*it);
}
學習處理模板化基類內的名稱
. 假設要編寫一個程式,功能是傳送資訊到不同的公司,不同的公司對資訊傳送的要求可能不同,因此將由多個類來表示不同的公司的資訊傳遞方式:
class CompanyA{
public:
...
void sendCleartext(const string& msg);
void sendEntrypted(const string& msg);
...
};
class CompanyB{
public:
...
void sendCleartext(const string& msg);
void sendEntrypted(const string& msg);
...
};
... //其他公司類
. 既然有這麼多公司,並且傳遞方式不同,那完全可以使用模板的方式來使用它們自己的函式:
template<typename Company>
class MsgSender{
public:
...
void sendClear(const string& info){
Company c;
c.sendCleartext(info);
}
...
};
. 現在這麼做沒問題,但假設需要在傳送資訊前後加上日誌資訊,可以對派生類加上這樣的功能:
template <typename Company>
class LogMsgSender : public MsgSender<Company>{
public:
...
void sendClearMsg(const string& info){
writetolog(); //傳送前寫入日誌
sendClear(info);
writetolog(); //傳送後寫入日誌
}
...
};
. 看上去合情合理,但是上述程式碼通不過編譯,編譯器會抱怨senClear不存在,這是因為當編譯器遭遇class template LogMsgSender 定義式時,並不知道它繼承自什麼樣的class,雖然寫出來的是MsgSender<Company>,但其中的Company是template引數,不到將它具現化的時候無法確切知道它是什麼,也就不知道它是否有sendClear函式。
更具體的原因是編譯器知道基類的模板可能被特化,比如有一個公司沒有sendCleartext這個函式,只有加密傳送的函式:
class CompanyZ{
public:
...
void sendEntrypted(const string& msg);
...
};
. 這就需要對它產生一個特化的版本:
template<> //該語法表示這既不是template也不是標準class
//而是一個特化版的MsgSender template
class MsgSender<CompanyZ>{
public:
...
void sendSecret(const string& info){
...
}
...
};
. 有了這樣的特化版的情況存在,派生類中就可能產生錯誤,比如派生類中的sendClearMsg函式類的Company為CompanyZ。
解決方法有三種:第一是在基類函式呼叫動作前加上this指標:
template <typename Company>
class LogMsgSender : public MsgSender<Company>{
public:
...
void sendClearMsg(const string& info){
writetolog(); //傳送前寫入日誌
this->sendClear(info);
writetolog(); //傳送後寫入日誌
}
...
};
. 第二是使用using宣告式:
template <typename Company>
class LogMsgSender : public MsgSender<Company>{
public:
using MsgSender<Company>::sendClear;
...
void sendClearMsg(const string& info){
writetolog(); //傳送前寫入日誌
sendClear(info);
writetolog(); //傳送後寫入日誌
}
...
};
. 第三種是明確指出被呼叫的函式位於基類中,但是這種方法不值得推薦,因為如果被呼叫的是虛擬函式,這樣的明確指出將關閉virtual繫結行為:
template <typename Company>
class LogMsgSender : public MsgSender<Company>{
public:
...
void sendClearMsg(const string& info){
writetolog(); //傳送前寫入日誌
MsgSender<Company>::sendClear(info);
writetolog(); //傳送後寫入日誌
}
...
};
將與引數無關的程式碼抽離template
. template程式設計節省時間和避免程式碼重複的一個好辦法,但是template還是可能帶來程式碼膨脹的:其二進位制碼帶著幾乎相同的程式碼、資料或是兩者。其結果可能原始碼看起來很合身且整齊,但是目標碼卻不是那麼回事。
避免這種二進位制浮誇的主要工具是:共性和變形分析。名字很好聽,但其實就是分析相同的部分和不同的部分,將相同的部分抽出來使原先的class共同使用,不同的保留在原位置不變。
比如,為固定尺寸的正方形矩陣編寫template,該矩陣的一個特性是支援逆矩陣運算:
template<typename T,size_t n> //n表示是n x n的矩陣
class SquareMatrix{
public:
...
void invert(); //求逆矩陣的函式
};
//具現化的時候
SquareMatrix<double,5> sm1;
sm1.invert(); //呼叫SquareMatrix<double,5>::invert
SquareMatrix<double,10> sm2;
sm2.invert(); //呼叫SquareMatrix<double,10>::invert
. template中除了有型別引數T,還有一個表示矩陣行列的非型別引數n,這樣的方式在具現化的時候出現了兩份invert,但這兩個函式除了常量5和10,其他部分應該是全部相同的,這就是造成程式碼膨脹的一個典型例子。
大部分時候我們的第一想法是為他們建立一個帶數值引數的函式,通過無數值引數的函式來呼叫這個函式,而不重複程式碼:
template<typename T>
class SquareMatrixBase{
protected:
...
void invert(size_t n);
...
};
template<typename T,size_t n>
class SqureMatrix:public SquareMatrixBase<T>{
private:
using SquareMatrixBase<T>::invert; //防止基類中的名稱被遮掩
public:
...
void invert(){
this->invert(n); //呼叫基類的函式
}
};
. 這樣確實減少了程式碼重複的問題,但是還有一個問題:基類的invert函式怎麼知道要操作的矩陣的資料?它只知道矩陣的尺寸,但是並不知道資料在哪裡,想必只有派生類知道,所以需要在兩者之間做一個聯絡工作。
一個辦法是在傳一個指標引數給函式,但是如果要用到矩陣資料的函式很多的話,需要逐個新增,這樣很不好。
另一個辦法是令基類存貯一個指標,指向矩陣數值所在的記憶體,這樣在建構函式中可以獲得應有的資料:
template<typename T>
class SquareMatrixBase{
protected:
SquareMatrixBase(size_t n,T* pMem)
:size(n),pData(pMem){}
void setDataPtr(T* ptr){
pData = ptr;
}
...
private:
size_t size;
T* pData;
};
//關於派生類,可以有兩種方式來決定記憶體分配方式
//------------------版本一----------------------
//不需要動態分配,但是可能導致物件自身很大
template<typename T,size_t n>
class SqureMatrix:public SquareMatrixBase<T>{
public:
SquareMatrix():SquareMatrixBase<T>(n,data){}
...
private:
T data[n*n]; //矩陣資料存貯在class內部
};
//===========版本二=============
template<typename T,size_t n>
class SqureMatrix:public SquareMatrixBase<T>{
public:
SquareMatrix()
:SquareMatrixBase<T>(n,0) //現將基類的資料指標置空
,pData(new T[n*n]){ //將指向該記憶體的指標儲存起來
this->setDataPtr(pData.get()) //然後將它的一副本交給基類
}
...
private:
boost::scoped_array<T> pData; //指向陣列的智慧指標
};
. 雖然減少程式碼膨脹是好事,但有時候盲目的追求精密,事情可能變得更加複雜,所以有時候一點點程式碼重複反而看起來幸運了。
其實型別引數也會導致膨脹,例如在很多平臺上int和long有相同的二進位制表述,所以想vector<int>和vector<long>的成員函式有可能完全相同,類似情況,在大多數平臺上,所有指標型別都有相同的二進位制表述。因型別引數造成的程式碼膨脹,往往可降低,做法是讓帶有相同二進位制表述的具現型別共享實現碼,比如成員函式操作強型指標(即T*)的情況,可以讓他們呼叫另一個操作無型別指標(即使void*)的函式。