天天看點

Effective C++ 第七章—— 模闆與泛型程式設計Effective C++ 第七章—— 模闆與泛型程式設計

Effective C++ 第七章—— 模闆與泛型程式設計

條款41——了解隐式接口和編譯器多态

    隐式接口(implicit interface):對于template而言所必須有效編譯的一組表達式,就是類型T必須支援的一組隐式接口;

    編譯器多态(compile-time polymorphism):以不同的template參數具現化的function templates 會導緻調用不同的函數。

    通常顯示接口由函數的簽名式(函數名稱、參數類型、傳回類型)構成。而隐式接口并不相同,它不基于簽名式,而是由有效的表達式組成。

    例如:

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

    doProcessing函數中,T必須提供一個名為size的函數,size隻需要傳回一個X類型的對象,而X對象加上一個int類型必須能夠調用operator>;T還需要支援operator != ,operator !=接受一個X類型和Y類型的對象,隻要T類型可以被轉換為X類型,someNastyWidget類型可以被轉換為Y類型。其他隐式接口:copy構造函數、normalize和swap函數也都必須對T類型有效。

請記住:

  • class 和template都支援接口(interface)和多态(polymorphism);
  • 對class而言接口都是顯示的(explicit),以函數簽名為中心。多态則是通過virtual函數發生于運作期;
  • 對于template參數而言,接口都是隐式的(implicit),奠基于有效表達式。多态則是通過template具現化和函數重載解析(function overloading resolution)發生于編譯期。

條款42——了解template的雙重意思

    在template聲明式中,class和typename沒有什麼不同。

  • 從屬名稱:template内出現的名稱如果相依于某個template參數,則稱為從屬名稱(dependent names);
  • 嵌套從屬名稱:從屬名稱在class内呈嵌套狀。
  • 謂非從屬名稱:一個不依賴template參數的名稱。

    例如:

template<typename C>
void print2nd(const C& container)
{
	if(container.size() >= 2)
	{
		C::const_iterator iter(container.begin());//C::const_iterator是嵌套從屬名稱
		++iter;
		int vlaue = *iter;//int是謂非從屬名稱
		std::cout<<value;
	}
}
           

    編譯器開始解析時,如果遇到一個嵌套從屬名稱,這是編譯器假設他不是一個類型,除非你告訴他是。

    給出一個一般性規則解決上述問題,任何時候當你想要在template中涉及一個嵌套從屬類型名稱,就必須在它的前一個為止上放置關鍵字typename;這一規則的例外是typename不可以出現在base class list内的嵌套從屬類型名稱之前,也不可以出現在membei initialization list(成員初值列)中作為base class修飾符。例如:

template<typename T>
class Derived: public Base<T>::Nested {  //base class list中不允許“typename”
public:
	explicit Derived(int x): Base<T>:: Nested(x)//成員初值列不允許“typename”
	{
		typename Base<T>::Nested temp;//嵌套從屬類型名稱,既不在base class list也不在
									//mem.init.list中,作為一個base class修飾符需要加上typename
		...
	}
	...
};
           

請記住:

  • 聲明template參數時,字首關鍵字class和typename可互換;
  • 請使用關鍵字typename辨別嵌套類從屬類型名稱但不得在base class lists(基類列)或member initialization list(成員初值列)内以它作為base class修飾符。

條款43——學習處理模闆化基類内的名稱

    當我們定義了一個模闆化的base class時,在定義一個模闆化的derived class後,在derived class中調用base class的成員函數時,編譯器是無法通過的;這是由于當編譯器遇到derived class定義式時,并不知道它繼承了什麼樣的class,不到後來(當derived class被具現化)無法确切知道他是什麼。往往拒絕在模闆化基類(templatized base classes)内尋找繼承而來的名稱。

class CompanyA{//公司A
public:
	...
	void sendCleartext(const std::string& msg);
	void sendEncrypted(const std::string& msg);
	...
};
class CompanyB{//公司B
public:
	...
	void sendCleartext(const std::string& msg);
	void sendEncrypted(const std::string& msg);
	...
};
...//針對其他公司的class
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){...}
};
template<typename Company>
class LoggingMsgSender: public MsgSender<Company> {
public:
	...
	void sendClearMsg(const MsgInfo& info)
	{
		将“傳送前”的資訊寫至log;
		sendClear(info);
		将“傳送前”的資訊寫至log;
	}
	...
};
           

    為了讓C++“不進入templatized base classes 的行為失效”有三種解決辦法:

  1. 在base class函數調用之前加上“this->”;
template<typename Company>
class LoggingMsgSender: public MsgSender<Company> {
public:
	...
	void sendClearMsg(const MsgInfo& info)
	{
		将“傳送前”的資訊寫至log;
		this->sendClear(info);
		将“傳送前”的資訊寫至log;
	}
	...
};

           
  1. 用using聲明式;
template<typename Company>
class LoggingMsgSender: public MsgSender<Company> {
public:
	using MsgSender<Company>::sendClear;
	...
	void sendClearMsg(const MsgInfo& info)
	{
		将“傳送前”的資訊寫至log;
		sendClear(info);
		将“傳送前”的資訊寫至log;
	}
	...
};

           
  1. 明确指出被調用函數位于哪個base class内;這是最不讓人滿意的一個做法,如果被調用的是virtual函數,上述明确指定資格修飾會關閉virtual的動态綁定行為。
template<typename Company>
class LoggingMsgSender: public MsgSender<Company> {
public:
   ...
   void sendClearMsg(const MsgInfo& info)
   {
   	将“傳送前”的資訊寫至log;        
   	MsgSender<Company>::sendClear(info);
   	将“傳送前”的資訊寫至log;
   }
   ...
};
           

請記住:

  • 可在derived class templates内通過“this->”指涉base class template内的成員名稱;或藉由一個明白寫出的“base class資格修飾符”完成。

條款44——将參數無關的代碼抽離template

請記住:

  • Template生成多個class和多個函數,是以任何template代碼都不該與某個造成膨脹的template參數産生相依關系;
  • 因非類型參數模闆而造成的代碼膨脹,往往可降低,往往可以消除,做法是以函數或class成員變量替換template參數。
  • 因類型參數(type parameters)而造成的代碼膨脹,往往可降低,做法是讓帶有完全相同二進制表述的具現類型共享實作代碼。

條款45——運用成員函數模闆接受所有相容類型

    成員函數模闆(member function templates)的作用是用于使class模闆相容所有對象,通常用于為構造函數寫一個模闆,或者支援指派操作。

    同一個template的不同具現體之間并不存在什麼與生俱來的關系。即使是一個模闆由base class和derived class分别具現化,這兩個具現化之間也是沒有什麼關系的。如果我們希望一個模闆由base class和derived class具現化後,能夠由derived class具現化的模闆轉換為base class具現化模闆,需要定義一個成員函數模闆。這裡有時候也稱為泛化copy構造函數。

template<typename T>
class SmartPtr{
public:
	template<typename U>
	SmartPtr(const SmartPtr<U>& other)//這裡采用成員初始化清單是為了,當存在某個隐式轉換可以将一個
									//U*指針轉換為一個T*指針才能通過編譯。
		:heldPtr(other.get()) {...};
	T* get() {return heldPtr;}
	...
private:
	T* heldPtr;
};
           

成員函數模闆(member function templates)支援指派操作,例如

template<typename T>
class shared_ptr {
public:
	template<class Y>    		//構造來自任何相容的
	 exlicit shared_ptr(Y* p);  //内置指針、
	template<class Y> 
	 shared_ptr(shared_ptr<Y> const& t);//shared_ptr、
	template<class Y> 
	 exlicit shared_ptr(weak_ptr<Y> const& t); //weak_ptr、
	template<class Y> 
	 exlicit shared_ptr(auto_ptr<Y> & t);//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
           

請記住:

  • 請使用 member function templates (成員函數模闆) 生成“可接受所有相容類型”的函數;
  • 如果你聲明member template 用于“泛化copy構造”或“泛化assgnment操作”,你還需要聲明正常的copy構造函數和copy assignment操作符。

條款46——需要類型轉換是請為模闆定義非成員函數

    為了使用一個函數模闆,我們需要在調用他之前先推導出參數類型,然後将函數進行适當的具現化。但是在template實參推導過程中是不考慮采納“通過構造函數而發生的”隐式類型轉換的 。

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;//錯誤,無法通過編譯!無法推斷出operator *的參數類型。
           

    為了解決上述template函數支援所有參數的隐式類型轉換,可以在template class内定義friend函數,并在template class内定義它,這樣才能正常的編譯并執行,如果不在class内定義則隻能編譯通過但無法連接配接。為了使inline所帶來的沖擊最小,如果inline的函數代碼塊太大的話,會影響程式的效率,這時我們可以令inline後的函數調用輔助函數,緩解壓力。

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

請記住:

  • 當我們編寫一個class template, 而它所提供之“與此template相關的” 函數支援“所有參數之隐式類型轉換”時,請将那些函數定義為“class template内部的friend函數”。

條款47——請使用traits class 表現類型資訊

    traits:一種技術,将它放進一個template及一個或多個特化版本中,一種template的應用。它要求對内置類型(主要是指針)和使用者自定義類型表現一樣好。

    當函數,類或者一些封裝的通用算法中的某些部分會因為資料類型不同而導緻處理或邏輯不同(而我們又不希望因為資料類型的差異而修改算法本身的封裝時),traits會是一種很好的解決方案。

    這樣的template在标準庫中有多個,其中針對疊代器者被命名為iterator_traits:

template<typename IterT>
struct iterator_traits
{
	typedef typename IterT::iterator_category iterator_category;
	...
};
//為了支援指針疊代器,iterator_traits針對指針提供一個偏特化版本。
template<typename IterT>
struct iterator_traits<IterT*>
{
	typedef random_access_iteraotr_tag iterator_category;
	...
};
           

設計一個traits class的方法

  • 确認若幹你希望将來可取得的類型相關資訊。對疊代器而言,我們希望可以取得其分類。
  • 為該資訊選擇一個名稱(例如iterator_category)
  • 提供一個template和一組特化版本,内涵你希望支援的類型的相關資訊。
//定義一個class方法compute,
//需要在T為int類型時,Compute方法的參數為int,傳回類型也為int;
//當T為float時,Compute方法的參數為float,傳回類型為int
//而當T為其他類型,Compute方法的參數為T,傳回類型也為T,怎麼做呢?還是用traits的方式思考下。
template <typename T>
struct TraitsHelper {
     typedef T ret_type;
     typedef T par_type;
};
template <>
struct TraitsHelper<int> {
     typedef int ret_type;
     typedef int par_type;
};
template <>
struct TraitsHelper<float> {
     typedef int ret_type;
     typedef float par_type;
};
//class定義
template <typename T>
class Test {
public:
     TraitsHelper<T>::ret_type Compute(TraitsHelper<T>::par_type d);
private:
     T mData;
};

           

使用traits class的方法

  • 建立一組重載函數(身份像勞工)或函數模闆,彼此之間的差異隻在于各自的traits參數,令每個函數實作碼與其接受的traits資訊相應和;
  • 建立一個控制函數(身份像工頭)或函數模闆,它調用上述那些勞工函數并傳遞traits class所提供的資訊。

    請記住:

  • Traits classes 使得“類型相關資訊”在編譯期可用。它們以tempalte 和“template特化”完成實作;
  • 整合重載技術(overloading)後,traits classes 有可能在編譯期對類型執行if…else測試。

條款48——認識template元程式設計

    Template metaprogramming(TMP ,模闆元程式設計):編寫template-based C++程式并執行與編譯期的過程。

    一個TMP的例子:

template<unsigned n>
struct Factorial {
	enum { value = n * Factorial<n-1>::value };
};
template<>
struct Factorial<0> {
	enum { value = 1 };
};
int main()
{
	std::cout<<Factorial<5>::value;//列印120;
	std::cout<<Factorial<10>::value;//列印3628800
	return 0;
}
           

請記住:

  • Template metaprogramming(TMP,模闆元程式設計)可以将工作由運作期移往編譯期,因而得以實作早期錯誤偵測和更高的執行效率;
  • TMP可被用來生成“基于政策選擇組合”的客戶定制代碼,也可以用來避免生成對某些特殊類型并不合适的代碼。

繼續閱讀