遞歸之美
- Loki庫TypeList源碼剖析
鄧
輝
TypeList
概觀提起
List,想必大家都不會陌生,它是一個元素的集合,並且提供了一些對該集合進行操作的方法,比如:計算集合中元素的個數、向集合中添加元素、獲取給定索引處的元素等。我們所熟知的List中的元素一般都是實例化後的值,相關的操作也都是在運行期間進行的。本文將要剖析的List和上述的List在某種意義上比較相象,不過它所包含的元素都是類型(type),相關的操作是在編譯期間進行的。本文將要講述的
TypeList取自Andrei Alexandrescu的力作《Modern C++ Design》一書相關的Loki庫,關於《Modern C++ Design》,C++的愛好者想必不會陌生,在該書中,作者向我們展現了C++設計的全新思維,把C++的表達能力發揮到了極至,而Loki庫正是這種思維的具體表現。TypeLsit是Loki庫中最爲基礎、核心的組件,理解了TypeList就具備了觀賞這道C++新景觀的基礎。下面我們先來看看如何定義一個
TypeList,這樣可以對於TypeList先有一個感性的認識。typedef TYPELIST_3(char, int, long) MyTypeList;
定義了一個具有三個元素的
TypeLsit,這三個元素分別爲:char、int、long。TYPELIST_3爲Loki庫中提供的用於定義TypeList的工具,我們會在本文的後面進行介紹。 ::Loki::Length<MyTypeList>::value; 計算MyTypeList中元素的個數,結果爲3。typedef ::Loki::TypeAt<MyTypeList, 1>::Result MyType;
獲取
MyTypeList中第1個元素(從0開始),此時MyType就是int。typedef ::Loki::Append<MyTypeList, float>::Result MyTypeList1;
向
MyTypeList中在添加一個元素:float,結果爲MyTypeList1。此時::Length<MyTypeList1>::value等於4。好了,先介紹這麼多,下面我們將介紹
TypeList實現的一些相關的背景知識,包括:遞歸的基本概念、模板的特化(tempalte specilization)、模板的偏特化(template partial specilization)以及類型萃取技術(type traits)。TypeList
相關技術
遞歸概述
對於遞歸,大家肯定都不陌生,使用遞歸方法給出的解決方案總是顯得非常的優雅、簡潔。不過遞歸方法所適合解決的問題應該符合下面的條件:
一個問題的解決依賴於一個較小規模的同樣的問題的解決 必須有一個明確的結束條件 這個結束條件是可達的
如果一個問題符合上述的三個條件,我們就可以使用遞歸的方法。首先我們定義一套解決問題的規則,接着縮小問題的規模並應用同樣的規則直到達到結束條件,然後結果層層返回直到原始問題。著名的漢諾塔問題就是一個典型的遞歸問題,如果不使用遞歸方法,解決漢諾塔問題就會顯得非常的複雜,晦澀。
我們在使用遞歸方法設計程序時,這個遞歸過程的調用總是在運行期間進行的。本文所介紹的
TypeList的實現中,遞歸的執行是在編譯期間進行的,那麼在編譯期間如何定義每次遞歸的返回結果,如何定義結束條件呢?其中主要使用了下面將要介紹的模板特化、偏特化以及類型萃取技術。
模板特化和偏特化(
template specilizaiton、partial specilization)什麼是模板的特化、偏特化呢?大致的意思爲:如果一個
template擁有一個或者一個以上的template參數,我們可以針對其中一個或者多個參數進行特化處理(如果全部進行特化處理就是全特化,否則就是偏特化,切記:函數模板只能進行全特化,不能進行部分特化)。也就是說,我們可以提供一個特別版本,符合泛化條件,但是其中某些(全部)template參數已經由實際類型或者數值取代。假設我們有一個
class template定義如下: template<class U, class V, class T>class C { … };
對於模板的偏特化,剛剛接觸可能會存在一些誤解:以爲模板的偏特化版本一定是對
template參數U或者V或者T(或者它們的任意組合)指定某個參數值。其實不是這樣的,所謂模板的偏特化是指另外提供一份template的定義,它的具體含義可以和通用的template定義版本無關。在一個偏特化版本中,template 參數的個數並不需要吻合通用的 template 中的個數。然而,出現於於class 名稱之後的參數個數必須吻合通用的 template 的參數個數。下面舉一個簡單的例子進行說明: template<class T>class C { … };
這個泛型版本允許T爲任意的類型。它的一個偏特化版本如下: template<class T>class C<T*> { … };
這個偏特化版本僅適用於T爲原生指針類型的情況。當我們使用
C<int>去定義一個變量時,編譯器會自動使用泛型版本,如果使用C<int*>去定義一個變量時,編譯器就會自動使用偏特化的版本。有了這個利器,我們就可以解決在編譯期間定義遞歸的結束條件的問題。
類型萃取(
type traits)類型萃取技術是泛型程序設計中的一個常用技術,它的思維核心爲:把一系列與類型相關的性質包裹於單一的
class 之內,這樣我們就可以在編譯期間獲取一些所需要的和該類型相關的東西。其實這個思路就是軟件領域一句著名的諺語:“任何事情都可以通過添加額外的中間層次得以解決”的又一次體現。通過把一系列想得到的類型相關的信息封裝在另外一個類型定義中,這樣就可以以一致的方式來對這些類型進行處理,提供了強大的可複用性和靈活性。類型萃取技術一般都和模板的特化、偏特化技術結合在一起運用,這樣它們就可以互相補充發揮出巨大的威力。下面簡單舉一個例子來了解一下類型萃取技術。
我們來看看
Boost庫中一個簡單的template<class T> class is_pointer的實現。我們需要一個主版本,用來處理T不爲指針的所有情況,以及一個偏特化版本,用來處理T是指針的情況:template <class T>
struct is_pointer
{ static const bool value = false; };
template <class T>
struct is_pointer<T*>
{ static const bool value = true; };
一個簡單的示例如下:
template<class T>void Func(T param)
{
if (is_pointer<T>::value) {
// do something
}
else {
//do something
}
…
}
通過類型萃取技術,我們就可以在編譯期間保留每次遞歸的結果,供遞歸返回時使用。
關於這些技術的更多、更深入的介紹,請讀者自行參考相關資料,不在此贅述。在下面的源碼剖析中,讀者將會看到這些技術的實際運用。
TypeList
實現剖析
有了上述的背景知識,下面我們就來揭開
TypeList的神祕面紗,走進TypeList的源碼世界。首先,我們來看一下TypeList的定義。TypeList定義
爲了能夠一致的進行
TypeList的操作,在Loki庫中定義了一個空類型NullType來標記一個TypeList的結束,NullType和TypeList的定義如下:class NullType
{ };template <class T, class U>
struct Typelist
{
typedef T Head;
typedef U Tail;
};
對於規範型
TypeList的定義採用了一種尾遞歸的方法:NullType是規範的TypeLsit 如果T是規範的TypeList,那麼對於任意原子類型U,TypeList<U,T>是規範的TypeList
Loki
庫中所採用的TypeList均爲規範型的TypeList,這樣可以在不減少靈活性的前提下簡化對於TypeList的操作。本文後面所指的TypeList均爲規範型的。如何定義一個
TypeList呢?比如:包含:char、int、long三個元素的TypeLsit。根據上面的定義,可以得到如下的定義形式: TypeList<char, TypeLsit<int, TypeLsit<long, NullType> > >; // 注意兩個>間一定要加一個空格,不//
然編譯器會認爲是 “>>” 操作符這樣的定義方法顯得比較麻煩、羅嗦,爲了簡化用戶對於
TypeList的使用,Loki庫中採用了宏定義的方式對於大小在1~50的TypeList進行了預定義: #define TYPELIST_1(T1) ::Loki::Typelist<T1, ::Loki::NullType>#define TYPELIST_2(T1, T2) ::Loki::Typelist<T1, TYPELIST_1(T2) >
#define TYPELIST_3(T1, T2, T3) ::Loki::Typelist<T1, TYPELIST_2(T2, T3) >
…
依此類推。
這樣用戶在使用起來就會比較方便一些。
TypeList典型操作實現
瞭解了
TypeList的定義,這裏我們將對於TypeList相關的三個典型操作(Length、TypeAt和Append)的實現進行詳細的剖析。掌握了這幾個典型的操作,再學習其他的操作就會變得非常的容易。我們將通過一個實例進行講解,來看一下編譯器實際的運作過程。我們定義了一個包含兩個元素:
int以及long的TypeList。typedef TYPELIST_2(int, long) MyTypeList;
此時,編譯器會根據
TypeList的定義方式產生如下的類型定義結果: struct TypeList<long, NullType>{
typedef long H; typedef NullType T;}
; struct TypeList<int , TypeList<long, NullType > >{
typedef int H;typedef TypeList<long, NullType> T;
}
;Length的實現 - 獲取TypeList中的元素個數:
template <class TList> struct Length; //
僅有聲明,沒有實現,如果所傳入的類型不是TypeLsit//
的話,會產生一個編譯期錯誤template <> struct Length<NullType> //
遞歸調用的結束條件,NullType的大小爲0,運用了{ //
模板特化和類型萃取技//
術enum { value = 0 };
};
template <class T, class U> //
遞歸的規則定義,運用了模板偏特化和類型萃取技術struct Length< Typelist<T, U> >
{
enum { value = 1 + Length<U>::value };
};
當通過
Length<MyTypeList>::value獲得MyTypeList中的元素個數時,看看編譯器是如何根據我們指定的規則進行遞歸調用的。首先編譯器會生成如下幾個版本的Length定以: struct Length<TypeList<long, NullType> >{
enum { value = 1 + Length<NullType>::value };}
; struct Length<TypeList<int, TypeList<long, NullType> > >{
enum { value = 1 + Length<TypeList<long, NullType> >::value };}
;根據
Length結束條件的定義可知,Length<NullType>::value等於0,所以Length<TypeList<long, NullType> >::value就等於Length<NullType>::value+1,也就是1。通過遞推可知,Length<MyTypeList>::value也就是Length<TypeList<int, TypeList<long, NullType> > >::value等於Length<TypeList<long, NullType> >::value+1,也就是2。在層層的遞推過程中,類型萃取技術得到了充分的體現,value就是我們想要得到的TypeList類型相關的信息,在每一層的遞歸過程中,都是通過它來保留結果的。TypeAT
的實現 - 獲取給定位置處的元素template <class TList, unsigned int index> struct TypeAt; //
僅有聲明,沒有實現,如果所傳入//
的類型不是TypeLsit//
的話,會產生一個編譯期錯誤template <class Head, class Tail>
struct TypeAt<Typelist<Head, Tail>, 0> //
遞歸調用的結束條件,如果給定位置爲0,則{ //
返回TypeList中的第一個元素typedef Head Result;
};
template <class Head, class Tail, unsigned int i> //
遞歸規則定義,注意這裏的返回結果爲類型,struct TypeAt<Typelist<Head, Tail>, i> //
運用了類型萃取技術。typename關鍵字的作{ //
用是告訴編譯器其後的實體是類型。typedef typename TypeAt<Tail, i - 1>::Result Result;
};
下面看看使用
TypeAt<MyTypeList, 1>::Result時,編譯器都產生了那些動作。首先編譯器要根據遞歸規則生成如下的類型定義:struct TypeAt<TypeList<long, NullType> , 0>
{
typedef long Result;
}
; struct TypeAt<TypeList<int , TypeList<long, NullType> > , 1>{
typedef TypeAt<TypeList<long, NullType> , 0>::Result Result;
}
;很明顯
typedef TypeAt<TypeList<long, NullType> , 0>::Result Result; 就是typedef long Result;所以,TypeAt<MyTypeList, 1>::Result就是long類型。同樣的,在這個實現中充分使用了類型萃取技術,不過,這裏我們想要的不是value,而是Result。
Append
的實現 - 在TypeList的末尾添加一個元素template <class TList, class T> struct Append; //
僅有聲明,沒有實現,如果所傳入//
的類型不是TypeLsit//
的話,會產生一個編譯期錯誤template <> struct Append<NullType, NullType> //
遞歸結束條件定義,模板的偏特化{
typedef NullType Result;
};
template <class T> struct Append<NullType, T> //
遞歸結束條件定義,模板的偏特化{
typedef TYPELIST_1(T) Result;
};
template <class Head, class Tail>
struct Append<NullType, Typelist<Head, Tail> > //
遞歸結束條件定義,模板的偏特化{
typedef Typelist<Head, Tail> Result;
};
template <class Head, class Tail, class T> //
遞歸規則定義,注意這裏的返回結果爲類型,struct Append<Typelist<Head, Tail>, T> //
運用了類型萃取技術。typename關鍵字的作{ //
用是告訴編譯器其後的實體是類型。模板的偏特//
化typedef Typelist<Head,
typename Append<Tail, T>::Result>
Result;
};
同樣的,讓我們來看看當使用
Append<MyTypeList, float>::Result時,編譯器的遞歸執行動作。首先看看編譯器會生成的一些類型定義: struct Append<NullType, float>{
typedef TYPELIST_1(float) Result;
}
;struct Append<TypeList<long, NullType> , float>
{
typedef TypeList<long, Append<NullType, float>::Result > Result;
};
struct Append<TypeList<int, TypeList<long, NullType> >, float>{
typedef TypeList<int, Append<TypeList<long, NullType>,float >::Result > Result;
}
;經過簡單的替換,
Append<MyTypeList, float>::Result就等於:typedef TypeList<int, Append<TypeList<long, NullType>,float >::Result >;
等於:
typedef TypeList<int, TypeList<long, Append<NullType, float>::Result > >;
等於:
typedef TypeList<int, TypeList<long, TypeList<float, NullType> > >;
也就是:TYPELIST_3(int, long, float)
;等同於在原有TypeList的末尾添加了一個新元素float。不用多說了,這裏類型萃取技術同樣發揮了巨大的作用。
結束語
本文對於
TypeLsit的實現進行了剖析,相信讀者朋友對於TypeList含義以及實現手法已經有所掌握。那麼TypeLsit到底有什麼用處呢?源碼面前,了無祕密,掌握了TypeList,就掌握了全面理解Loki庫的鑰匙。在Loki庫中,你將會看到Abstract Factory模式、Visitor模式這些泛型組件是如何在TypeLsit的基礎上搭建起來的。背起你的行囊,拿起這把鑰匙,趕快踏上你的“尋寶”之路吧(Loki庫的源代碼可以從www.moderncppdesign.com下載)。