the boost c++ metaprogramming

THE BOOST C++ METAPROGRAMMING<?xml:namespace prefix = o ns = "urn:schemas-microsoft-com:office:office" />

LIBRARY

Aleksey Gurtovoy

MetaCommunications

[email protected]

David Abrahams

Boost Consulting

[email protected]

?

1. Introduction

元程序通常被定義爲“生成其它程序的程序”;像YACC那樣的Parser generators是元程序的一種;YACC的輸入是符合Extended Backus-Naur Form [EBNF]規範語法的語言,輸出則是能夠解析這種語法的程序(即這種語法的Parser);注意這個例子中,元程序(YACC)是由一種不能夠直接描述被生成程序的語言寫就的(C語言),而我們稱作metadata的那些規範,卻是由一種meta-language寫的,而不是由C語言;由於用戶程序的剩餘部分通常需要一種general-purpose的編程系統,並且必須與生成的Parser交互,因此metadata被翻譯成了C語言,進而與系統的剩餘部分編譯連接在一起;metadata因此經歷兩個翻譯階段,用戶不得不一直有意識的維護metadata和程序剩餘部分之間的界限

?

1.1. Native language metaprogramming

元程序更有意思的一種形式存在於像Scheme那樣的語言中,在那裏,被生成程序的規範描述是由與元程序本身同樣的語言給出的;元程序員定義的meta-language是所用編程語言語法的一個子集,被生成程序的生成可以和用戶程序剩餘部分的處理髮生在同一個翻譯階段;這就允許用戶透明的在“元程序”,“被生成程序”,和“普通程序”之間切換,並且通常不知道這種轉換

?

1.2. Metaprogramming in C++

C++中,幾乎是偶然的,模板機制被發現提供了豐富的編譯期計算的機制;

在這一節中,我們將探索C++元編程的基本機制和常用技術

?

1.2.1. Numeric computations

non-type template parameters的使編譯期進行整數運算成爲可能;例如,下面的模板計算了它的參數的階乘(編譯期):

template< unsigned n >

struct factorial

{

static const unsigned value = n * factorial<n-1>::value;

};

template<>

struct factorial<0>

{

static const unsigned value = 1;

};

上面的代碼片斷被稱爲“metafunction”,很容易看出它和運行期計算的函數之間的聯繫:metafunction的“參數”是作爲模板參數傳遞的,“返回值”是由嵌套的靜態常量來定義的;因爲C++中編譯期表達式和運行期表達式之間的hard line,元程序看起來不同於它運行期的對應物;像在Scheme中一樣,C++元程序員用和普通程序一樣的語言(C++)來寫元程序,但是只用到C++全部語言特性的一個子集:那些可以在編譯期計算的表達式;用直接運行期定義的階乘函數來比較一下上面的程序:

unsigned factorial(unsigned N)

{

return N == 0 ? 1 : N * factorial(N - 1);

}

很容易看出兩個遞歸定義的類似;通常來說,遞歸對C++元編程比對運行期編程更重要;與像Lisp那樣遞歸是慣用法的語言相比,C++程序員通常需要儘可能的避免遞歸;這不僅僅是因爲效率問題,還因爲“文化因素”:遞歸是難以理解的(對C++程序員);雖然如此,但就像pureLisp,這種C++模板機制是一種函數式編程語言;同樣的,它也消除了使用循環變量來維護循環的用法;

運行期和編譯期階乘函數的一個關鍵不同是遞歸結束條件的表達式:

我們的meta-factorial使用了模板特化作爲一種模式匹配機制來描述當N等於0時的行爲

運行期世界中的對應物將需要同一個函數的兩種單獨的定義(N等於0N不等於0

在這個元程序中,第二個函數模板(特化的那個)定義的影響是很小的,但在大的元程序中,理解和維護結束條件的代價將變得巨大

另外注意一個C++ metafunction的返回值必須“命名”;這個例子中選擇的名字“value”,也是MPL中所有數值返回值所使用的名字;就像我們將要看到的,爲metafunction的返回值建立統一的命名機制對庫獲得強大的功能是至關重要的

?

1.2.2. Type computations

我們將如何使用我們的factorial metafunction呢?for example,我們可以產生一個適當大小的數組類型來容納另外一種類型的對象的實例的所有排列組合

// permutation_holder::type is an array type which can contain

// all permutations of a given T.

// unspecialized template for scalars

template< typename T >

struct permutation_holder

{

typedef T type[1][1];

};

// specialization for array types

template< typename T, unsigned N >

struct permutation_holder

{

typedef T type[factorial::value][N];

};

這裏我們引入了“類型計算”的概念

像上面的factorialpermutation_holder模板是一個metafunction

然而,區別於factorial操作的是無符號整數數值permutation_holder接受和“返回(作爲嵌套的typedef類型)”一個類型由於C++類型系統提供了較使用non-type template argument (e.g. the integers)所做的事情遠爲豐富的表達式集合,C++元程序大部分是由類型計算組成的

?

1.2.3. Type sequences

程序化的操作類型集合的能力是很多有意義的C++元程序的重要的工具

因爲這種能力MPL支持的很好,這裏我們僅僅簡要的介紹一下基礎的東西

後面,我們將重新回顧下面的例子,並演示如何使用MPL來實現

首先,我們需要一種方法來表示這個容器;一個主意是用structure來存儲類型:

struct types

{

int t1;

long t2;

std::vector t3;

};

不幸的是,這種安排無法使用C++給我們的編譯期類型內省的能力(類似JavaReflection

沒有辦法找出成員的名字是什麼,即使我們假定名字是按照上面的慣用法給出的,我們也沒有辦法知道有多少個成員;解決這個問題的關鍵是提高表示的一致性;如果我們有一個方法可以得到任何序列的第一個類型,和剩餘的序列,那麼我們將輕易的獲取所有成員:

template< typename First, typename Rest >

struct cons

{

typedef First first;

typedef Rest rest;

};

struct nil {};

typedef

cons

, cons

, cons

, nil

> > > my_types;

上面的my_types所描述的結構是單向鏈表的編譯期對應物,是由Czarnecki and Eisenecker in [CE98]首先引進的;現在,我們已經調整了最初的結構,此時C++模板機制能夠一層層的剝開它,我們來看一個完成這個功能的簡單的metafunction;假設用戶希望找到任意類型集合中最大的一個類型;我們可以使用現在已經很熟悉的遞歸的metafunction

?

Example 1. 'largest' metafunction

// choose the larger of two types

template<

typename T1

, typename T2

, bool choose1 = (sizeof(T1) > sizeof(T2)) // hands off!

>

struct choose_larger

{

typedef T1 type;

};

// specialization for the case where sizeof(T2) >= sizeof(T1)

template< typename T1, typename T2 >

struct choose_larger< T1,T2,false >

{

typedef T2 type;

};

// get the largest of a cons-list

template< typename T > struct largest;

// specialization to peel apart the cons list

template< typename First, typename Rest >

struct largest< cons >

: choose_larger< First, typename largest::type >

{

// type inherited from base

};

// specialization for loop termination

template< typename First >

struct largest< cons >

{

typedef First type;

};

int main()

{

// print the name of the largest of my_types

std::cout

<< typeid(largest::type).name()

<< std::endl

;

}

這段代碼中有幾個地方值得注意:

它使用了一些ad-hoc,深奧的技術,或者“hacks

缺省模板參數choose1(“hands off!”做了標記)就是一個例子;沒有它,我們將需要另外一個模板來提供choose_larger的實現,或者我們不得不顯式的計算後作爲參數提供給模板,對這個例子也許不算太壞,但它將使choose_larger用處更少,更加容易出錯

另一個hack是從choose_larger中分出特化的largest;這是一種減少代碼的措施,將使程序員避免在模板體中編寫typedef typename ...::type type

?

即使這麼簡單的元程序也使用了三個單獨的偏特化

largest metafunction使用了兩個特化;有人可能期待着這意味着有兩個結束條件,但不是這樣的:其中一個特化只是用來簡單的處理對序列元素的存取;這些特化通過將單個metafunction的定義散佈在幾個C++模板的定義中而使代碼難以閱讀;並且,因爲它們是偏特化,對於C++社區中其編譯器不支持偏特化的廣大的程序員來說,它們是不可用的

?

然而,這些技術理所當然是任何好的C++元程序員武器庫中很有價值的部分;通過封裝常用的結構,內部處理循環結束,MPL減少了tricky hacks和模板特化的需要

?

1.3. Why metaprogramming?

問一下人們爲什麼想這麼做是有意義的;畢竟,即使像factorial metafunction這樣的玩具程序也有些深奧;爲了演示類型計算如何應用在工作中,我們再來看一個簡單的例子:

下面的代碼產生了一個數組,用來容納另一個數組其元素所有可能的排列組合

// can't return an array in C++, so we need this wrapper

template< typename T >

struct wrapper

{

T x;

};

// return an array of the N! permutations of 'in'

template< typename T >

wrapper< typename permutation_holder::type >

all_permutations(T const& in)

{

wrapper::type> result;

// copy the unpermutated array to the first result element

unsigned const N = sizeof(T) / sizeof(**result.x);

std::copy(&*in, &*in + N, result.x[0]);

// enumerate the permutations

unsigned const result_size = sizeof(result.x) / sizeof(T);

for (T* dst = result.x + 1; dst != result.x + result_size; ++dst)

{

T* src = dst - 1;

std::copy(*src, *src + N, *dst);

std::next_permutation(*dst, *dst + N);

}

return result;

}

factorial的運行期版本在上面的all_permutations中是無法使用的,因爲在C++中數組大小必須在編譯期計算;然而,另有一些替補方案,我們如何才能避免元編程,最終的結論又是什麼呢?

?

1,我們可以寫程序直接解釋元數據

在我們的factorial的例子中,數組大小可以是運行期的變量,因而我們能夠直接使用運行期的factorial函數,但是這意味着動態分配,而它通常是昂貴的;更進一步,可以改寫YACC讓它接受一個函數指針,這個指針指向的函數將返回待解析流中的tokens和包含文法描述的字符串;然而,對大多數程序來說,這種方法將導致不可接受的運行期代價:解析器或者是不得不接受不確定的文法,爲每一次解析都摸索一遍文法,或者是不得不爲每次輸入的文法在運行期複製the substantia table-generation和優化已經存在的YACC的工作

?

2,我們可以用我們自己的計算來代替編譯器編譯期間的計算

畢竟,傳遞給all_permutations的數組的大小一直是編譯期可知的,因此對用戶也是可知的,我們可以要求用戶顯式的提供結果的類型:

template< typename Result, typename T >

Result all_permutations(T const& input);

這種方法的代價是很明顯的:我們放棄了表達能力(通過要求用戶顯式的指定實現細節)和正確性(通過允許用戶指定錯誤的結果類型);任何一個手工寫過解析器表格的人將告訴你,這種方法的不切實際正是YACC存在的原因

?

在元數據可以以用戶其餘程序同樣的語言來表達的語言,如C++中,表達能力得到了進一步的加強:用戶可以直接調用元程序,不需要學習另外的語法,不需要打斷自己正常的代碼流

?

因此,元編程的動機來自於下面三個因素的聯合:效率,表達能力,正確性;在傳統的程序中,一直存在着一股壓力,一邊是表達能力和正確性,一邊是效率,而在元編程世界中,我們揮舞着新的武器:我們能夠將表達能力所需要的計算從運行期轉移到編譯期

?

1.4. Why a metaprogramming library?

有人可能還想問一下爲什麼我們需要一個泛型庫:

?質量

適用於通用目的的庫的代碼,往往也適用於用戶的目的,對庫的開發者來說,這是中心任務;一般來說,任何C++標準庫的實現給出的容器和算法,都要比大量存在的特定工程的實現給出的容器和算法更加靈活,實現的更好,因爲庫的開發是以它本身爲目的的,而不是作爲其他應用附帶的任務,

對任何給定功能的集中的實現,更容易應用優化和改進

?複用

比任何庫都提供的代碼的複用更重要,一個設計良好的泛型庫建立了一個conceptsidiomsframework,這些conceptsidioms建立了解決問題的可複用的思維的模型;類似於C++ STL給了我們iterator conceptsfunction object protocolBoost MPL提供了type-iterators metafunction class protocol;一個考慮周全的idiomsframework節省了元程序員考慮與手頭工作不相關的實現細節的時間,而讓他們集中精力在手頭問題上

?可移植性

一個優秀的庫可以消除,掩蓋醜陋的平臺差異;理論上一個元程序庫是完全泛型的,不需考慮這些問題,但實際上對模板的支持依然有大量的不一致,即使是標準化四年之後;這或許不應該感到驚奇:C++模板是語言最深遠,最複雜的特性,極大的增強了C++元編程的能力

?有趣

一遍遍的重複同樣的idioms是單調乏味的;它使程序員疲勞,降低生產力;更進一步,當程序員感到厭倦時他們會以比慢慢寫代碼更大的代價寫出晦澀的,充滿蟲子的代碼;通常最有用的庫是機敏的程序員從海量的重複中提取出的簡單的模式;MPL通過消除大量樣板代碼的重複來幫助程序員減少厭倦;

?

像人們能夠看到的,MPL的開發是由推動了其它庫的開發的同樣的,實際的,真實世界中的問題推動的;或許這是一種跡象,表明模板元編程已經完成了最後的準備,即將離開深奧的領域,進入每個日常程序員的口頭話題

發佈了27 篇原創文章 · 獲贊 0 · 訪問量 6萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章