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*)的函數。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章