遞歸之美 - Loki庫TypeList源碼剖析

 

遞歸之美

LokiTypeList源碼剖析

 

TypeList

概觀

提起

List,想必大家都不會陌生,它是一個元素的集合,並且提供了一些對該集合進行操作的方法,比如:計算集合中元素的個數、向集合中添加元素、獲取給定索引處的元素等。我們所熟知的List中的元素一般都是實例化後的值,相關的操作也都是在運行期間進行的。本文將要剖析的List和上述的List在某種意義上比較相象,不過它所包含的元素都是類型(type),相關的操作是在編譯期間進行的。

本文將要講述的

TypeList取自Andrei Alexandrescu的力作《Modern C++ Design》一書相關的Loki庫,關於《Modern C++ Design》,C++的愛好者想必不會陌生,在該書中,作者向我們展現了C++設計的全新思維,把C++的表達能力發揮到了極至,而Loki庫正是這種思維的具體表現。TypeLsitLoki庫中最爲基礎、核心的組件,理解了TypeList就具備了觀賞這道C++新景觀的基礎。

下面我們先來看看如何定義一個

TypeList,這樣可以對於TypeList先有一個感性的認識。

typedef TYPELIST_3(char, int, long) MyTypeList;

定義了一個具有三個元素的

TypeLsit,這三個元素分別爲:charintlongTYPELIST_3Loki庫中提供的用於定義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 specilizaitonpartial 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的結束,NullTypeTypeList的定義如下:

class NullType

{ };

template <class T, class U>

struct Typelist

{

typedef T Head;

typedef U Tail;

};

對於規範型

TypeList的定義採用了一種尾遞歸的方法:

NullType是規範的TypeLsit

如果T是規範的TypeList,那麼對於任意原子類型UTypeList<UT>是規範的TypeList

Loki

庫中所採用的TypeList均爲規範型的TypeList,這樣可以在不減少靈活性的前提下簡化對於TypeList的操作。本文後面所指的TypeList均爲規範型的。

如何定義一個

TypeList呢?比如:包含:charintlong三個元素的TypeLsit。根據上面的定義,可以得到如下的定義形式:

TypeList<char, TypeLsit<int, TypeLsit<long, NullType> > >; // 注意兩個>間一定要加一個空格,不

//

然編譯器會認爲是 >> 操作符

這樣的定義方法顯得比較麻煩、羅嗦,爲了簡化用戶對於

TypeList的使用,Loki庫中採用了宏定義的方式對於大小在150TypeList進行了預定義:

#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相關的三個典型操作(LengthTypeAtAppend)的實現進行詳細的剖析。掌握了這幾個典型的操作,再學習其他的操作就會變得非常的容易。

我們將通過一個實例進行講解,來看一下編譯器實際的運作過程。我們定義了一個包含兩個元素:

int以及longTypeList

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下載)。
發佈了28 篇原創文章 · 獲贊 1 · 訪問量 17萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章