C++進階_Effective_C++第三版(七) 模板與泛型編程 Templates and Generic Programming

模板與泛型編程 Templates and Generic Programming

C++模板的最初設計目的是建立類型安全的容器如vector,list和map。後來發現模板有能力完成越來越多可能的變化。後面就出現了泛型編程,寫出的代碼和其所處理的對象類型彼此獨立。STL算法如for_each,find和merge就是這類編程的成果。後面發現它可以被用來計算任何可計算的值。於是導出了模板元編程。創造出在C++編譯器內執行並於編譯完成時停止執行的程序。

41、瞭解隱式接口和編譯期多態

Understand implicit interfaces and compile-time polymorphism.
面向對象編程世界總是以顯式接口和運行期多態解決問題。舉個例子,給定這樣的class:

class Widget{
public:
	Widget();
	virtual ~Widget();
	virtual std::size_t size() const;
	virtual void normalize();
	void swap(Widget& other);};
// 和這樣的函數:
void doProcessing(Widget& w)
{
	if(w.size() >10 && w != someNastyWidget)
	{
		Widget temp(w);
		temp.normalize();
		temp.swap(w);
	}
}

對於doProcessing內的w,由於w的類型被聲明爲Widget,所以w必須支持Widget接口,可以在源碼中找到這個接口,這就爲一個顯式接口,由於Widget的某些成員函數是virtual,w對那些函數的調用將表現出運行期多態,也就是說將於運行期根據w的動態類型決定究竟調用哪個函數。
Templates及泛型編程中,和麪向對象有根本上的不同。顯式接口和運行期多態仍然存在,但重要性降低。隱式接口和編譯期多態移到前面。如將上述的doProcessing從函數變成函數模板:

template<typename T>
void doProcessing(T& w)
{
	if(w.size() >10 && w != someNastyWidget)
	{
		T temp(w);
		temp.normalize();
		temp.swap(w);
	}
}

現在對於doProcessing內的w,w必須支持的接口由template中執行於w身上的操作來決定。本例看來w的類型T好像必須支持size,normalize和swap成員函數、copy構造函數(用來建立temp)、不等比較。重要的是這一組表達式便是T必須支持的一組隱式接口。凡涉及w的任何函數調用,例如operator>和operator!=,有可能造成template具現化,使這些調用得以成功。這樣這樣的具現化發生在編譯期。“以不同的template參數具現化”會導致調用不同的函數,這便是所謂的編譯期多態。
通常顯式接口由函數的簽名式(也就是函數名稱、參數名稱、返回類型)構成。隱式接口則完全不同,它並不基於函數簽名式,而是由有效表達式組成。如上面模板函數doProcessing,T的隱式接口有這些約束:必須提供一個名爲size的成員函數,該函數返回一個整數值。必須支持一個operator!=函數,用來比較兩個T對象等等。加諸於template參數身上的隱式接口,就像加諸於class對象身上的顯式接口一樣,而且兩者都在編譯期完成檢查。就和無法以一種“與class提供的顯式接口矛盾”的方式來使用對象(代碼將通不過編譯),也無法在template中使用“不支持template所要求之隱式接口”的對象(代碼將通不過編譯)。

  • classes和templates都支持接口和多態。
  • 對classes而言接口是顯式的,以函數簽名爲中心。多態則是通過virtual函數發生於運行期。
  • 對template參數參數而言,接口是隱式的,奠基於有效表達式。多態則是通過template具現化和函數重載解析發生於編譯期。

42、瞭解typename的雙重意義

Understand the two meanings of typename.
很多時候typename和class是等價的,意義完全相同。但是也有例外。假設有個模板函數,接受一個STL兼容容器爲參數,容器內持有的對象可被賦值爲ints,然後打印其第二個元素值:

template<typename C>
void print2nd(const C& container)	//打印容器內的第二元素
{							//這不是有效的C++代碼
	if(container.size() >= 2)
	{
		C::const_iterator iter(container.begin());	//取第一個元素的迭代器
		++iter;			
		int value = *iter;		//將該元素複製到某個int
		std::cout << value;
	}
}

template內出現的名稱如果相依於某個template參數,稱之爲從屬名稱。如果從屬名稱在class內呈嵌套狀,稱爲嵌套從屬名稱如C::const_iterator。實際它還是個嵌套從屬類型名稱,也就是個嵌套從屬名稱並且指涉某類型。而變量value不依賴任何template參數,稱爲非從屬名稱。嵌套從屬名稱有可能導致解析困難如:

template<typename C>
void print2nd(const C& container)	
{
	C::const_iterator* x;}

看起來好像聲明x爲一個local變量,是個指針,指向一個C::const_iterator。但是如果C::const_iterator不是一個類型且x是個全局變量名稱,上述代碼成了一個相乘動作。爲了解除歧義,必須顯式的聲明其爲類型,只需要給其前面加上typename即可:

template<typename C>
void print2nd(const C& container)
{
	if(container.size() >= 2)
	{
		typename C::const_iterator iter(container.begin());}
}

Typename只被用來驗明嵌套從屬類型名稱,其他名稱不該有其存在。如下函數模板接受一個容器和一個指向該容器的迭代器:

template<typename C>				//允許使用typename或class
void f(const C& container,			//不允許使用typename
typename C::iterator iter);	//一定要使用typename

上述的C並不是嵌套從屬類型名稱,所以聲明container時並不需要以typename爲前導,但C::iterator是個嵌套從屬名稱類型,所以必須以typename爲前導。typename必須作爲嵌套從屬類型名稱的前綴詞的例外時typename不可以出現在基類list內的嵌套從屬類型名稱之前,也不可在member initialization list(成員初值列)中作爲基類修飾符。例如:

template<typename T>
class Derived: public Base<T>::Nested{	//基類list中不允許typename
public:
	explicit Derived(int x):Base<T>::Nested(x)	//成員初值列不允許typename
	{
		typename Base<T>::Nested temp;	//作爲一個基類修飾符需加上typename}};

假設寫一個函數模板,它接受一個迭代器,然後爲該迭代器指涉的對象做一份local復件temp:

template<typename IterT>
void workWithIterator(IterT iter)
{
	typename std::iterator_traits<IterT>::value_type tewmp(*iter);}

std::iterator_traits::value_type是標準traits類的一種運用,相當於類型爲IterT的對象所指之物的類型。這個語句聲明一個local變量(temp),使用IterT對象所指物的相同類型,並將temp初始化爲iter所指物。如果IterT是vector::iterator,temp的類型就是int。

  • 聲明template參數時,前綴關鍵字class和typename可互換。
  • 使用關鍵字typename標識嵌套從屬類型名稱,但不得在基類列或成員初值列內以它作爲基類修飾符。

43、學習處理模板化基類內的名稱

Know how to access names in templatized base classes.
假設有個程序,能夠傳送信息到若干不同的公司去。信息要不譯成密碼,要不就是未加工的文字。如果編譯期間我們有足夠信息來決定哪個信息傳遞哪個公司,用基於template實現:

class CompanyA{
public:void sendCleartext(const std::string& msg);
	void sendEncrypted(const std::string& msg);};
class CompanyB{
public:void sendCleartext(const std::string& msg);
	void sendEncrypted(const std::string& msg);};class MsgInfo	{};	//保存信息,以備將來產生信息
template<typename Company>
class MsgSender{
public:void sendClear(const MsgInfo& info)
	{
		std::string msg;
		//根據info產生信息
		Company c;
		c.sendCleartext(msg);
	}
	void sendSecret(const MsgInfo& info)	//類似sendClear,唯一不同調用c.sendEncrypted
	{}
};

假設想在每次送出信息時記錄日誌。derived class實現如下:

template<typename Company>
class LoggingMsgSender: public MsgSender<Company>{
public:void sendClearMsg(const MsgInfo& info)
	{
		//將傳送前的信息寫至log
		sendClear(info);		//調用基類函數,代碼是不能通過編譯的。
		//將傳送後的信息寫至log
	}
};

這個派生類的信息傳遞函數有一個不同的名稱sendClearMsg,與其基類內的名稱sendClear不同,可以避免遮掩繼承而得的名稱,也避免重新定義一個繼承而得的非虛函數。但上述代碼不能通過編譯,是因爲編譯器在處理class template LoggingMsgSender定義式時,並不知道它繼承什麼樣的class。雖然看起來繼承MsgSender,但其中的Company是個template參數,不到LoggingMsgSender被具現化無法確切知道它是什麼。而且如果不知道Company是什麼,就無法知道class MsgSender看起來像什麼,更明確地說是沒辦法知道它是否有個sendClear函數。
爲解決此問題,有三個辦法:第一在基類函數調用動作之前加上this->:

template<typename Company>
class LoggingMsgSender: public MsgSender<Company>{
public:void sendClearMsg(const MsgInfo& info)
	{
		//將傳送前的信息寫至log
		this->sendClear(info);		//假設sendClear將被繼承下來。
		//將傳送後的信息寫至log
	}
};

第二是使用using聲明式:

template<typename Company>
class LoggingMsgSender: public MsgSender<Company>{
public:
	using MsgSender<Company>::sendClear;void sendClearMsg(const MsgInfo& info)
	{
		//將傳送前的信息寫至log
		sendClear(info);		//假設sendClear將被繼承下來。
		//將傳送後的信息寫至log
	}
};

第三是明確指出被調用的函數位於基類內:

template<typename Company>
class LoggingMsgSender: public MsgSender<Company>{
public:void sendClearMsg(const MsgInfo& info)
	{
		//將傳送前的信息寫至log
		MsgSender<Company>::sendClear(info); //假設sendClear將被繼承下來。
		//將傳送後的信息寫至log
	}
};

第三個做法是最不可取的,因爲如果被調用的是個virtual函數,上述的明確資格修飾會關閉virtual綁定行爲。

  • 可在派生類模板內通過this->指涉基類模板內的成員名稱,或由一個明白寫出的基類資格修飾符完成。

44、將與參數無關的代碼抽離template

Factor parameter-independent code out of template.
Templates是節省時間和避免代碼重複的一個奇方妙法。但是使用template可能會導致代碼膨脹,其二進制碼帶着重複的代碼、數據。一般編寫某個class,然後發現和另一個class的某些部分相同,把共同的部分搬移到新類中,然後使用繼承或複合令原先的類取用這些共同特性。編寫template時,也是做相同的分析,以相同的方式避免重複,但在template代碼中,代碼重複時隱晦的,因爲只存在一份template源碼,需要自己感受當template被具現化多次時可能發生的重複。
假設爲固定尺寸的正方矩陣編寫一個template。該矩陣的性質之一是支持逆矩陣運算。
template<typename T, std::size_t n> //template支持n*n矩陣,元素類型爲T的對象。

class SquareMatrix{
public:void invert();	//求逆矩陣
};

這個template接受一個類型參數T,除此之外還接受一個類型爲size_t的參數,那是個非類型參數。這種參數和類型參數比起來較不常見,但完全合法。如下使用:

SquareMatrix<double, 5> sm1;
…
sm1.invert();
SquareMatrix<double, 10> sm2;
…
sm2.invert();

上述代碼會具現化兩份invert。這些函數並非完全相同,但是除了常量5和10,其它部分完全相同。這種現象是template造成代碼膨脹。
對於此可以採用下述實現:

template<typename T >	//與尺寸無關的基類
class SquareMatrixBase{
protected:void invert(std::size_t matrixSize);	//求逆矩陣
};
template<typename T, std::size_t n>	
class SquareMatrix: private SquareMatrixBase<T> {
private:
	using SquareMatrixBase<T>:: invert
public:void invert(){	this->invert(n);	 };	//求逆矩陣
};

這種做法利用SquareMatrixBase:: invert使用protected避免代碼重複。調用它而造成的額外成本是0,因爲派生類的invert調用基類版本時用的是inline調用。但是這個需要每次告訴基類矩陣尺寸,需要額外添加一個額外參數,不夠友好。所以可以讓SquareMatrixBase儲存一個指針,指向矩陣數值所在的內存,只有它存儲了那些東西,也就可能存儲矩陣尺寸:

template<typename T, std::size_t n>	
class SquareMatrix: private SquareMatrixBase<T> {
public:
	SquareMatrix() : SquareMatrixBase<T>(n,data) {	}private:
	T data[n*n]; 
};

這種類型的對象不需要動態分配內存,但對象自身可能非常大。另一個做飯是把每一個矩陣的數據放進heap:

template<typename T, std::size_t n>	
class SquareMatrix: private SquareMatrixBase {
public:
	SquareMatrix() : SquareMatrixBase<T>(n,0),pData(new T[n*n])
	{	this->setDataPtr(pData.get());	}private:
	boost::scoped_array<T> pData; 
};
  • Templates生成多個classes和多個函數,所以任何template代碼都不該與某個造成膨脹的template參數產生相依關係。
  • 因非類型模板參數而造成的代碼膨脹,往往可消除,做法是以函數參數或class成員變量替換template參數。
  • 因類型參數而造成的代碼膨脹,往往可降低,做法是讓帶有完全相同二進制表述的具現化類型共享實現碼。

上一篇: C++進階_Effective_C++第三版(六) 繼承與面向對象設計 Inheritance and Object-Oriented Design
下一篇: C++進階_Effective_C++第三版(八) 定製new和delete Customizing new and delete

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