深入淺析C++ traits技術
前言
traits,又被叫做特性萃取技術,說得簡單點就是提取“被傳進的對象”對應的返回類型,讓同一個接口實現對應的功能。因為STL的算法和容器是分離的,兩者通過迭代器鏈接。算法的實現並不知道自己被傳進來什麼。萃取器相當於在接口和實現之間加一層封裝,來隱藏一些細節並協助調用合適的方法,這需要一些技巧(例如,偏特化)。最後附帶一個小小的例子,應該能更好地理解 特性萃取。
下面大部分來源於《STL源碼剖析》,看原書能瞭解更多細節。
Traits編程技法
讓我們一點點拋出問題,然後一點點深入。
1. 首先,在算法中運用迭代器時,很可能會用到其相應型別(迭代器所指之物的型別)。假設算法中有必要聲明一個變量,以“迭代器所指對象的型別”為型別,該怎麼辦呢?
解決方法是:利用function template的參數推導機制。
template <class I, class T> void func_impl(I iter, T t) { T tmp; // 這裡就是迭代器所指物的類型新建的對象 // ... 功能實現 } template <class I> inline void func(I iter) { func_impl(iter, *iter); // 傳入iter和iter所指的值,class自動推導 } int main() { int i; func(&i); }
這裡已經可以看出封裝的意思瞭,沒有一層impl的封裝的話,每次你都要顯式地說明迭代器指向對象型別,才能新建tmp變量。加一層封裝顯得清爽很多。
迭代器相應型別不隻是“迭代器所指對象的型別”一種而已。根據經驗,最常用的相應型別有五種,然而並非任何情況下任何一種都可以利用上述的template參數推導機制來取得。
函數的“template參數推導機制”推導的隻是參數,無法推導函數的返回值類型。萬一需要推導函數的傳回值,就無能為力瞭。
2. 聲明內嵌型別似乎是個好主意,這樣我們就可以直接獲取。
template <class T> struct MyIter { typedef T value_type; // 內嵌型別聲明 // ... }; template <class I> typename I::value_type func(I ite) { return *ite; } // ... MyIter<int> ite(new int(8)); cout << func(ite);
看起來不錯,但是並不是所有迭代器都是class type,原生指針就不行!如果不是class type,就無法為它定義內嵌型別。
這時候就需要 偏特化 出現。
3. 偏特化就是在特化的基礎上再加一點限制,但它還是特化的template。
template <class I> struct iterator_traits { typedef typename I::value_type value_type; }; template <class I> struct iterator_traits<T*> { typedef T value_type; }; template <class I>12 typename iterator_traits<I>::value_type func(I ite) { return *ite; }
func在調用 I 的時候,首先把 I 傳到萃取器中,然後萃取器就匹配最適合的 value_type。(萃取器會先匹配最特別的版本)這樣當你傳進一個原生指針的時候,首先匹配的是帶<T*>的偏特化版本,這樣 value_type 就是 T,而不是沒有事先聲明的 I::value_type。這樣返回值就可以使用 typename iterator_traits<I>::value_type 來知道返回類型。
下面附上《STL源碼剖析》的圖片:
讓traits幹更多東西
迭代器有常見有五種類型: value_type, difference_type, reference_type, pointer_type都比較容易在 traits 和 相應偏特化中提取。但是,iterator_category一般也有5個,這個相應型別會引發較大規模的寫代碼工程。
例如,我們實現瞭 func_II, func_BI, func_RAI 分別代表迭代器類型是Input Iterator,Bidirectional Iterator和Random Access Iterator的對應實現。
現在,當客端調用func()的時候,我們可能需要做一個判斷:
template<class Iterator> void func(Iterator& i) { if (is_random_access_iterator(i)) func_RAI(i); if (is_bidirectional_iterator(i)) func_BI(i); else func_II(i); }
但這樣在執行時期才決定使用哪一個版本,會影響程序效率。最好能夠在編譯期就選擇正確的版本。
重載這個函數機制可以達成這個目標。
struct input_iterator_tag {}; struct output_iterator_tag {}; struct forward_iterator_tag : public input_iterator_tag {}; // ... // 繼承的好處就是,當函數需要用 input_iterator_tag 的時候 // 假設你傳進一個forward_iterator_tag,它會沿繼承向上找,知道符合條件
聲明瞭一些列 tag 之後,我們就可以重載 func函數: func(tag)。
到這裡,各個型別的具體重載實現已經寫好,但是需要一個統一的接口,這時候 traits 就可以出場瞭。
template<class Iterator> inline void func(Iterator& i) { typedef typename Iterator_traits<Iterator>::iterator_category category; __func(i, category()); // 各型別的重載 }
簡單實例代碼
所以說,traits一方面,在面對不同的輸入類時,能找到合適的返回型別;另一方面,當型別對應有不同的實現函數的時候,能起到一個提取型別然後分流的作用。
先假設我們有一個 func 函數,可以接受 自定義的類 或者 原始的指針 作為參數,並自動輸出使用瞭什麼tag。
首先根據 traits(由本身或偏特化版本實現) ,它會提取 u 的返回型別,然後調用對應的構造函數 return_type(), 來當作各個重載版本 __func 的重載標志區分不同的實際函數。
首先我們看看接口代碼的編寫
template <class unknown_class> inline typename unknown_class_traits<unknown_class>::return_type // 萃取器取得對應型別 func(unknown_class u) { typedef typename unknown_class_traits<unknown_class>::return_type return_type; return __func(u, return_type()); // 需要調用構造函數當tag }
然後是實現設定的 tag ,用來模仿前面說的 II,RAI等
template <class unknown_class> inline typename unknown_class_traits<unknown_class>::return_type return_type(unknown_class) { typedef typename unknown_class_traits<unknown_class>::return_type RT; return RT(); }
有瞭這些我們就可以測試瞭
struct A {}; struct B : A{};
然後是 traits 隆重登場,有兩個偏特化版本。
/*特性萃取器*/ template <class unknown_class> struct unknown_class_traits { typedef typename unknown_class::return_type return_type; }; /*特性萃取器 —— 針對原生指針*/ template <class T> struct unknown_class_traits<T*> { typedef T return_type; }; /*特性萃取其 —— 針對指向常數*/ template <class T> struct unknown_class_traits<const T*> { typedef const T return_type; };
突然忘記瞭交代 unknown_class 的結構,自定義的類,必須要 typedef。
template <class AorB> struct unknown_class { typedef AorB return_type; };
最後是func各個重載版本。
template <class unknown_class> inline typename unknown_class_traits<unknown_class>::return_type __func(unknown_class, A) { cout << "use A flag" << endl; return A(); } template <class unknown_class> inline typename unknown_class_traits<unknown_class>::return_type __func(unknown_class, B) { cout << "use B flag" << endl; return B(); } template <class unknown_class, class T> T __func(unknown_class, T) { cout << "use origin ptr" << endl; return T(); }
有瞭這些我們就可以測試瞭
int main() { unknown_class<B> b; unknown_class<A> a; //unknown_class<int> i; int value = 1; int *p = &value; A v1 = func(a); B v2 = func(b); int v3 = func(p); char ch = getchar(); }
可以看到,對於用自定義類傳入同一個接口,它會自動使用對應的函數,而且返回值也合適。對原始指針也適用,完美!
附
下面是完整代碼:
#include <iostream> using namespace std; /*先定義一些tag*/ struct A {}; struct B : A{}; // 繼承的好處就是,當函數需要參數為A, // 而你傳入的參數為B的時候,可以往上一直找到適合的對象 /*假設有一個未知類*/ template <class AorB> struct unknown_class { typedef AorB return_type; }; /*特性萃取器*/ template <class unknown_class> struct unknown_class_traits { typedef typename unknown_class::return_type return_type; }; /*特性萃取器 —— 針對原生指針*/ template <class T> struct unknown_class_traits<T*> { typedef T return_type; }; /*特性萃取其 —— 針對指向常數*/ template <class T> struct unknown_class_traits<const T*> { typedef const T return_type; }; /*決定使用哪一個類型*/ template <class unknown_class> inline typename unknown_class_traits<unknown_class>::return_type return_type(unknown_class) { typedef typename unknown_class_traits<unknown_class>::return_type RT; return RT(); } template <class unknown_class> inline typename unknown_class_traits<unknown_class>::return_type __func(unknown_class, A) { cout << "use A flag" << endl; return A(); } template <class unknown_class> inline typename unknown_class_traits<unknown_class>::return_type __func(unknown_class, B) { cout << "use B flag" << endl; return B(); } template <class unknown_class, class T> T __func(unknown_class, T) { cout << "use origin ptr" << endl; return T(); } template <class unknown_class> inline typename unknown_class_traits<unknown_class>::return_type func(unknown_class u) { typedef typename unknown_class_traits<unknown_class>::return_type return_type; return __func(u, return_type()); } int main() { unknown_class<B> b; unknown_class<A> a; //unknown_class<int> i; int value = 1; int *p = &value; A v1 = func(a); B v2 = func(b); int v3 = func(p); char ch = getchar(); }
結束語
特性提取花瞭自己好多時間,不過當程序跑出來的瞬間還是挺開心的。
首先要感謝侯捷老師,老師的書講得這麼清楚,我還是笨笨的看得一知半解。
看完這個可以看圖像的傅裡葉變換啦,啊哈哈~
以上就是C++ traits技術淺談的詳細內容,更多關於C++ traits技術的資料請關註WalkonNet其它相關文章!
推薦閱讀:
- 從c++標準庫指針萃取器談一下traits技法(推薦)
- 解析C++11的std::ref、std::cref源碼
- 詳解c++中的trait與policy模板技術
- 詳解C++右值引用
- 一文搞懂c++中的std::move函數