1. 程式人生 > >effective C++筆記--模板與泛型程式設計(二)

effective C++筆記--模板與泛型程式設計(二)

文章目錄

運用成員函式模板接受所有相容型別

. 真實指標做的很好的一件事是支援隱式轉換,派生類的指標可以指向基類的指標,指向非常量物件的指標可以指向轉換成常量物件的指標等。但是如果想在使用者自定義的智慧指標中模擬上述轉換,稍稍會有點麻煩。例如:

class Top{...};
class Middle:public Top{...};
class Bottom:public Middle{...};

template <typename T>
class SmartPtr{
public:
	explicit SmartPtr(T* realPtr);		//智慧指標通常以內建指標完成初始化
};
SmartPtr<Top> pt1 = SmartPtr<Middle>(new  Middle);
SmartPtr<Top> pt2 = SmartPtr<Bottom>(new  Middle);
SmartPtr<const Top> pct1 = pt1;

. 可惜上述程式碼不能通過編譯,因為同一個template的不同具現體之間並不存在什麼與生俱來的固有關係,所以編譯器將SmartPtr<Middle>和SmartPtr<Top>視為完全不同的class。因此為了獲得轉換能力,必須將它們明確的編寫出來。
  在上述的智慧指標例項中,每一條語句創造了一個新的智慧指標物件,所以應該關注的是如何讓編寫智慧指標的建構函式,使其行為能滿足轉型需要。但是一個很關鍵的觀察結果是:無法寫出所有需要的建構函式,即使根據現有的SmartPtr<Middle>和SmartPtr<Bottom>能構造SmartPtr<Top>,但是在以後新的類假如這個繼承體系的時候,還需要繼續新增,就原理而言,這樣需要的建構函式將沒有止境。因此,需要的不是給SmartPtr寫一個建構函式,而是為他寫一個構造模板。

這樣的模板就是所謂的成員函式模板,其作用是為class生成函式:

template <typename T>
class SmartPtr{
public:
	template <typename U>
	SmartPtr(const SmartPtr<U>& other);		//為了生成拷貝建構函式
};

. 以上程式碼的意思是,對任何型別T和任何型別U,可以根據SmartPtr<U>生成一個SmartPtr<T>。上述泛化的拷貝建構函式並沒有被宣告為explicit,因為原始指標之間的轉換是隱式轉換的,無需明確寫出轉型動作。
  完成聲明後,這個泛化建構函式提供的東西比需要的更多。我們的目的應該只是希望根據SmartPtr<Bottom>建立SmartPtr<Top>,但是不希望SmartPtr<Top>建立SmartPtr<Bottom>,這對public繼承而言是矛盾的。同時也不希望根據一個SmartPtr<double>建立SmartPtr<int>,因為現實中沒有將int轉換成double

的隱式轉換,所以,必須從某方面對這些建構函式進行篩選。
  假設SmartPtr遵循auto_ptr和tr1::shared_ptr所提供的榜樣,也提供一個get成員函式,返回智慧指標物件所持有的那個原始指標的副本,那麼可以在構造模板實現程式碼中進行約束,使它更合理:

template <typename T>
class SmartPtr{
public:
	template <typename U>
	SmartPtr(const SmartPtr<U>& other)		//以other的heldPtr來
		:heldPtr(other.get()){...}			//初始化this的heldPtr
	T* get() const {return heldPtr;}
private:
	T* heldPtr;						//所持有的內建(原始)指標
};

. 使用initialzation list來初始化SmartPtr<T>內的型別為T的成員變數,並以型別為U的指標作為初值,這個行為只有在“存在某個隱式轉換可將一個U指標轉為一個T指標”時才能通過編譯,這正是我們想要的。
  成員函式模板的效用不限於建構函式,另一個常用的效用是支援賦值操作,例如shared_ptr支援所有“來自相容之內建指標、shared_ptrs、auto_ptrs和
weak_ptrs“的構造行為,以及所有來自上述物的賦值操作,比如shared_ptr的一部分定義:

template<class T>
class shared_ptr{
public:
	template<class Y>
	explicit shared_ptr(Y* p);				//構造,來自任何相容的內建指標
	template<class Y>
	shared_ptr(shared_ptr<Y> const& r);		//或shared_ptr
	template<class Y>
	explicit shared_ptr(weak_ptr<Y> const& r);	//或weak_ptr
	template<class Y>
	explicit shared_ptr(auto_ptr<Y> const& r);		//或auto_ptr
	template<class Y>
	shared_ptr& operator=(shared_ptr<Y> const& r);	//賦值,來自
													//任何相容的shared_ptr
	template<class Y>								
	shared_ptr& operator=(auto_ptr<Y>& r);			//或auto_ptr
	
};

. 上述程式碼表示從某個shared_ptr隱式轉換至另一個shared_ptr是允許的但從某個內建指標或是其他智慧指標進行隱式轉換則不被認可。另外關於auto_ptr的複製建構函式和賦值操作符都未被宣告為const的,這是因為當你複製一個auto_ptr,其實它已經被改動了。
  成員函式模板並不改變語言基本規則,在class內宣告一個泛化的拷貝建構函式並不會阻止編譯器聲生成自己的拷貝建構函式(如果你沒有自己宣告的話),相同的規則也適用於賦值操作。

需要型別轉換時請為模板定義非成員函式

. 如之前的條款”若所有的引數皆需要型別轉換,請為此採用non-member函式“一樣,可能在模板程式設計的時候也能支援類似的複數之類的混合運算。所以可能寫出以下程式碼:

template<typename T>
class Rational{
public:
	Rational(const T& numerator = 0,const T& denominator = 1);
	const T numerator() const;
	const T denominator() const;
};

template<typename T>
const Rational<T> operator* (const Rational<T>& lhs,const Rational<T>& rhs){
	...
}

//呼叫
Rational<int> oneHalf(1,2);				
Rational<int> result = oneHalf * 2;			

. 遺憾的是,兩個呼叫都會報錯,看來模板化的Rational內的某些東西似乎和非模板的版本有所不同。事實也確實如此,在非模板的版本中,編譯器知道我們嘗試呼叫什麼函式,但在這裡,編譯器不知道想要呼叫什麼函式,取而代之的是,它試圖想出什麼函式被名為operator的template具現化出來。它們知道應該可以具現化出某個”名為operator並接受兩個引數“的函式,但為了完成這個操作,必須要知道T是啥,可惜它沒這個能耐。
  以上述程式碼來分析:傳給operator函式的第一個引數被宣告為Rational<T>,傳遞給它的第一個實參是Rational<int>,所以T是int,但是第二個引數被宣告為Rational<T>,傳遞給它的第二個實參是int,編譯器沒辦法根據這個推斷出T,或許你希望編譯器能自己做隱式轉換,但是在template的實參推導過程中從不將隱式型別轉換函式納入考慮
  只要利用一個事實,就可以緩和template實參推導方面遇到的挑戰:template class內的friend宣告式可以指涉某個特定函式。那意味著class Rational<T>可以宣告operator
是它的一個friend函式。class template並不依賴template實參推導,所以編譯器總是能夠在class template<int>具現化的時候得知T:

template<typename T>
class Rational{
public:
	...
	//不宣告為Rational<T>只是為了看起來簡潔點
	friend const Rational operator*(const Rational& lhs,const Rational& rhs);
};

template<typename T>
const Rational<T> operator*(const Rational<T>& lhs,const Rational<T>& rhs){
	...
}

. 這樣編寫後,能通過編譯,但是不能通過連結,這是因為這個函式只是被宣告與Rational內,並沒有被定義出來,雖然意圖在外部的operator* template提供定義式,但是行不通——如果我們自己聲明瞭一個函式,就有責任定義它。既然沒有提供定義式,自然連結出錯。
  最簡單的方式是將它的函式本體合併到宣告式內:

template<typename T>
class Rational{
public:
	...
	friend const Rational operator*(const Rational& lhs,const Rational& rhs){
		return Rational(lhs.numerator() * rhs.numerator(),
						lhs.denominator() * rhs.denominator());
	}
};

. 萬幸,對operator的呼叫現在可編譯連線並執行了。
  定義於class內部的函式都暗自成為inline函式,包括像operator
這樣的friend函式,可以將inline宣告帶來的衝擊最小化,做法是令operator不做任何事,只調用一個定義於class外部的輔助函式,但在本例中沒有意義,因為operator已經是一個單行函數了,但對更復雜的函式是可以試試的。這樣的輔助函式通常也是一個template,會宣告在標頭檔案中,並且許多編譯器實質上會強迫你把所有的template定義式都放入標頭檔案內。