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定義式都放入頭文件內。

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