effective C++筆記--模板與泛型編程(三)

請使用traits classes表現類型信息

. traits並不是C++的關鍵字或是預先定義好的構件,它們是一種技術,也是一個C++程序員共同遵守的協議。這項技術的要求之一是:它對內置類型和用戶自定義類型的表現必須一樣好,即traits技術能夠施行於用戶自定義類型,也能施行於內置類型如指針身上。
  “traits必須能施行於內置類型”意味着“類型內的嵌套信息”這種東西出局了,因爲我們無法將信息嵌套於原始指針內(PS:聽說C++20之後要刪除原始指針了)。因此類型的traits信息必須位於類型自身外。標準技術是將它放進一個template以及其一個或多個特化版本中。這樣的template在標準庫中有若干個,其中針對迭代器的被命名爲iterator_traits:

template<typename IterT>			//template,用來
struct iterator_traits;			//處理迭代器分類的相關信息

. 這裏iterator_traits是一個struct,實際上traits總是被實現化struct,但是往往被稱爲traits classes。
  那這種技術所指的分類的相關信息是什麼呢?舉個例子,在STL中迭代器有5種分類:
  -input迭代器:只能向前移動,一次一步,客戶只可讀取它們所指的內容,而且只能讀取一次,例如C++程序庫中的istream_iterators;
  -output迭代器:只能向前移動,一次一步,客戶只可塗寫它們所指的內容,而且只能塗寫一次,例如C++程序庫中的ostream_iterators;
  -forward迭代器:這種迭代器可以做上述兩種分類所能做的每一件事,而且可以讀或寫其所指物一次以上;
  -bidirectional迭代器:這種迭代器比上一個分類威力更大:它除了可以向前移動,還可以向後移動。例如STL的list迭代器、set、map的迭代器等;
  -random access迭代器:這種迭代器比上一個迭代器的威力更大的地方在於它可以執行“迭代器算術”,也就是它可以在常量時間裏向前或向後跳躍任意距離。例如vector、deque和string的迭代器。
  對於這五種分類,C++標準庫分別提供專屬的卷標結構加以確認:

struct input_iterator_tag{};
struct output_iterator_tag{};
struct forward_iterator_tag : public input_iterator_tag{};
struct bidirectional_iterator_tag : public forward_iterator_tag{};
struct random_access_iterator_tag : public bidirectional_iterator_tag{};

其中有的能實現隨機訪問,有的不能,因此要將迭代器進行移動多個單位的時候,有的只需要使用一次+=,有的只能通過多次++來實現,在函數模板中,traits便是通過判斷迭代器類型給出不同的方法。
  對於這五種分類,C++標準庫分別提供專屬的卷標結構加以確認:
  iterator_traits的運作方式是,針對每個類型的IterT,在struct iterator_traits<IterT>內一定聲明某個typedef爲iterator_category。就用這個typedef來確認IterT的迭代器分類。
  iterator_traits以兩個部分實現上述所言。首先它要求每一個“用戶自定義的迭代器類型”必須嵌套一個typedef,名爲iterator_category,用來確認適當的卷標結構。例如deque的迭代器可以隨機訪問,所以一個針對deque的迭代器設計的class看起來如下:

template<...>			//省略沒寫template參數
class deque{
public:
	class iterator{
	public:
		typedef random_access_iterator_tag iterator_category;
		...
	};
	...
};

. 置於iterator_traits,只是鸚鵡學舌般的相應typedef:

template<typename IterT>
struct iterator_traits{
	//IterT說它自己是什麼,就相信是什麼
	typedef typename IterT::iterator_categroy iterator_category;
	...
};

. 這對用戶自定義類型行的通,但是對指針(也是一種迭代器)不行,因爲指針不能嵌套typedef。因此iterator_tratis的第二部分專門用來對付指針,它提供一個偏特化的版本,由於指針的行徑與random access迭代器類似,所以爲指針指定的迭代器類型是:

template<typename IterT>
struct iterator_traits<IterT*>{
	typedef random_access_iterator_tag iterator_category;
	...
};

. 在有了iterator_traits後,你可能在判斷迭代器類型時使用if條件判斷:

//IterT是一個模板參數
if(typeid(typename std::iterator_traits<IterT>::iterator_category)
	== typeid(std::random_access_iterator_tag))
	...

. 看上去沒毛病,但這並不是我們想要的,首先這會導致編譯問題,還有一個更根本的問題是:IterT類型在編譯期間就獲知,所以iterator_traits<IterT>::iterator_category也可以在編譯期間獲知,但是if語句卻在運行期纔會覈定。爲什麼編譯期完成的事要延遲到運行期才做呢?那樣不僅浪費時間,也可能造成可執行文件膨脹。
  取而代之的一種方法是用到C++的重載。當重載一個函數時,必須詳細的敘述各個重載版本的參數類型。當調用函數時,編譯器就根據傳來的實參選擇最合適的重載版本。這正是針對類型所發生的“編譯期條件語句”。因此針對五種類型的迭代器所實現不同版本的函數可以如下編寫:

template <typename IterT,typename DistT>			//用於random access迭代器
void doAdvance(IterT& iter,DistT& d,std::random_access_iterator_tag){
	iter += d;
}
template <typename IterT,typename DistT>			//用於bidirectional迭代器
void doAdvance(IterT& iter,DistT& d,std::bidirectional_iterator_tag){
	if(d > 0){
		while(d--){
			++iter;
		}
	}
	else{
		while(d++){
			--iter;
		}
	}
}
template <typename IterT,typename DistT>			//用於input迭代器
void doAdvance(IterT& iter,DistT& d,std::input_iterator_tag){
	if(d<0){
		throw std::out_of_range("不能是負數");
	}
	while(d--){
		++iter;
	}
}

. 由於forward_iterator_tag繼承自input_iterator_tag,所以最後那個版本也能處理forward迭代器。有了這些重載版本後,調用這些函數時只需要額外傳遞一個對象,後者必須帶有適當的迭代器分類,例如:

template <typename IterT,typename DistT>
void advance(ItertT& iter,DistT d){
	doAdvance(iter,d,typename std::iterator_traits<IterT>::iterator_category());
}

. 現在可以總結如何使用一個traits class了:
  1.建立一組重載函數或函數模板(如doAdvance),彼此間的差異只在於各自的traits參數。令每個函數實現碼與其接受的traits信息相應;
  2.建立一個控制函數或函數模板(如advance),它調用上述那些函數並傳遞traits class所提供的信息。

認識模板元編程

. Template metaprograming(TMP,模板元編程)是編寫template-based C++程序並執行於編譯期的過程。所謂模板元程序是以C++編寫,執行於編譯器內的程序。一旦TMP程序結束執行,其輸出(也就是從template具現出來的若干C++源碼)便會一如往常的被編譯。
  TMP有兩個偉大的作用。第一:它使得某些事情變得更加簡單;第二:由於TMP執行於C++編譯期,因此可將工作從運行期轉換至編譯期,這導致的一個結果是某些錯誤可以更早的被發現,但是編譯的時間將會變得更加長,另一個結果是使得程序更加高效:較小的可執行文件、較短的運行期、較少的內存需求。
  上一個條款中的advance函數中介紹了兩種方法來判斷迭代器的種類:通過typeid的方式和重載的方式。推薦的是後者,其效率會更加高,因爲那就是TMP。另外typeid的方式將會帶來編譯的錯誤,以下就是例子:

std::list<int>::iterator iter;
...
advance(iter,10);					//移動iter向前走10個元素
									//上述實現無法通過編譯

//針對以上的調用,產生的template版本如下
void advance(std::list<int>::iterator& iter,int n){
	if(typeid(typename std::iterator_traits<std::list<int>::iterator>::iterator_category)
	== typeid(std::random_access_iterator_tag)){
		iter += n;								//錯誤
	}
	else{
		if(d > 0){
			while(d--){
				++iter;
			}
		}
		else{
			while(d++){
				--iter;
			}
		}
	}
}

. 問題出在使用了+=操作符的那行代碼,那嘗試在list<int>::iterator上使用+=,但是它並不支持,只有random access迭代器才支持+=,此刻我們知道絕不會執行+=那一行,因爲typeid那一行總是會因爲list<int>::iterator而失敗,但是編譯器必須確保所有的源碼都有效,縱使是不會執行的代碼。
  TMP已被證明是個強大到足以計算任何事物的機器。使用TMP你可以聲明變量、執行循環、編寫及調用函數…但這般構件相對於“正常的”C++對應物看起來很是不同。針對TMP設計的程序庫(如Boost‘s MPL)提供更高層級的語法——儘管目前還是不足以讓你誤以爲是“正常的”C++。
  比如在TMP中是怎麼實現循環的呢?TMP沒有真正的循環構件,所有循環效果是由遞歸來實現的,TMP主要是個“函數式語言”(我想起學了一週的erlang)。TMP的遞歸甚至不是正常種類,因爲TMP循環並不涉及遞歸函數調用,而是涉及“遞歸模板具現化”。
  像學習hello world一樣看看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<<endl;
}

. 當然TMP還能做更多更有用處的事情,下面舉三個例子:
  1.確保度量單位的正確。在科學和工程應用程序中,確保度量單位的正確結合是很重要的事情,比如將一個質量變量賦值給一個速度變量時不對的,但是可以將一個距離變量除以一個時間變量後的結果賦值給一個速度變量。如果使用TMP,就能確保在編譯期程序中所有度量單位的組合都正確,不論其計算多麼複雜;
  2.優化矩陣運算(唉!不懂)。假設有多個矩陣,求它們相乘的結果,如:
 Martrix<double,1000> m1,m2,m3,m4,m5;
 …
 Martrix<double,1000> = m1m2m3m4m5;
  以“正常的”函數調用動作來計算,會創建4個暫時性矩陣,每一個用來存儲對operator*的調用結果,並且各自的乘法產生4個作用於矩陣元素身上的循環。如果使用高級、與TMP相關的template技術,即所謂expression template,就能消除那些臨時變量併合並循環。於是TMP軟件使用較少的內存,卻又提高了執行速度。
  3.可以實現客戶定製之設計模式實現品。設計模式如Strategy,Observer等都可以使用多種方式實現出來。運用TMP-based技術,有可能產生一些template用來表述獨立的設計選項,然後可以任意結合它們,導致模式實現品帶着客戶定製的行爲。

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