-
Boost中的MPL庫分析
MPL(Meta-Programming Library)是由David Abrahams和Aleksey Gurtovoy爲方便模板元編程而開發的庫,2003年被Boost吸納爲其中的一員,此後又歷經一些大幅度修改,目前已經相當完善,其最新版本於2004年11月發佈。MPL的出現是C++模板元編程發展中的一大創舉,它提供了一個通用、高層次的編程框架,其中包括了序列(Sequence)、迭代器(Iterator)、算法(Algorithm)、元函數(Metafunction)等多種組件,具有高度的可重用性,不但提高了模板元編程的效率,而且使模板元編程的應用範圍得到相當的擴展。
(一)MPL的組織架構
一個庫的組織形式有時候甚至比它的功能還重要。MPL的作者聰明地借鑑了已經取得巨大成功的STL,在MPL中保留了許多STL的概念,對函數式的編程方式進行了精巧的包裝,使得任何熟悉STL的程序員都可以輕易地理解MPL的使用方法。像STL一樣,MPL有一個完整的概念體系,對組件作了精心的劃分,組件之間相對獨立,接口具有通用性,因此將組件之間的依存度和耦合性降低到最小的限度。
STL和MPL的組件概念對照如下:
STL概念
|
MPL對應概念
|
容器(Container) |
序列(Sequence) |
算法(Algorithm) |
算法(Algorithm) |
迭代器(Iterator) |
迭代器(Iterator) |
仿函數(Functor) |
元函數類(Metafunction) |
配接器(Adaptor) |
有View、Inserter Iterator和相當於仿函數配接器的Binding元函數 |
配置器(Allocator) |
無此概念 |
標準中沒有定義 |
宏(Macro) |
(二)MPL對其他庫的依賴
MPL是一個高層次的庫,它的地位和編譯期執行的特殊性決定了它需要一些特殊的輔助設施,並對其他庫會有所依賴。
1.Boost的Preprocessor庫
Preprocessor庫是一個基於宏的元編程庫[7]。預處理器的作用發生在編譯以前,所以它比MPL所處的地位還要高端,能夠真正實現代碼生成。它的典型功能是迭代或者枚舉相似的代碼段,減少重複而易寫錯的代碼段。MPL中不少代碼是近似的,比如在vector的原始代碼中,就需要定義n個
vectori { … }
其中i從1迭代到n。爲了減少重複勞動,MPL的源代碼大量使用自定義和Preprocessor庫的宏對重複或具有遞推性的內容進行迭代。不過,這也導致源代碼難以閱讀。比如上面一段展開後的源代碼首先是定義在vector/aux_/numbered.cpp的:
然後爲了迭代n個上面的類模板,另一個文件則需要重複include這個文件,利用Preprocessor的文件迭代能力可以這樣寫:
儘管如此,宏還是必需的,它不但避免了重複編寫遞推式的代碼(比如在上述的vector類模板中,n可達50之大,如果完全手寫確實是浪費時間),而且還有效控制了代碼的生成(比如只需要通過定義迭代次數,即可控制實際生成的類模板個數)。實際上,在使用vector(或其他組件)時,通常我們並不需要每次編譯都把這些代碼重新生成一次,MPL的作者已經充分考慮到編譯效率的問題,所以在MPL的代碼中,爲每個流行的編譯器都建立了一個Processed目錄,裏面存放着針對編譯器特點展開了的代碼。僅當定義了BOOST_MPL_CFG_NO_PREPROCESSED_HEADERS時纔會強制MPL重新用宏來生成代碼。
MPL的作者指出,無論喜歡還是不喜歡,目前宏必須在MPL中扮演着這個不可替代的角色。
2.Boost的Type Traits庫
Type Traits庫[9]用於驗證傳遞的參數或參數之間是否符合一定的條件,比如可以判定兩個參數是否有繼承關係、是否可轉換等。
3.Boost的Static Assert庫
Static Assert庫[8]用於編譯時斷言,用法類似於C中常用的斷言assert()。如果參數經編譯時的靜態計算爲true,則代碼能通過編譯,不會有任何效果,反之,則會編譯出錯,並且在使編譯信息裏面包含有“STATIC_ASSERTION_FAILURE”的字樣。
Static Assert的底層是接受一個bool參數的模板STATIC_ASSERTION_FAILURE,它對true定義一個有成員的特化模板,對false的情況則只有一個特化的聲明(無定義)。其接口是一個宏,它產生的代碼是sizeof(STATIC_ASSERTION_FAILURE< ... >),顯然當參數的實際結果爲false時,編譯器無法判斷STATIC_ASSERTION_FAILURE的長度,因爲它尚未定義。
因爲MPL是隻在編譯時生效的庫,用Static Assert來調試程序是非常合適的,它往往與Type Traits庫搭配使用。
4.Boost的Config庫
像STL一樣,由於編譯器對標準支持不同,爲了使程序庫具有移植性,最好是針對環境進行預先的設置。對於MPL這種先鋒性的庫來說,編譯器問題更加讓庫作者相當頭痛。藉助於對環境的偵查,可以對預先發現的問題,比如模板的局部特化能力、已知的一些編譯器的bug等等,採取相應的補救措施[4]。
(三)MPL中的序列
1.MPL序列概述
序列是MPL中的數據結構的統稱,是MPL中處於中心地位的組件,其地位相當於STL中的容器。MPL對序列的性質進行了細緻的劃分:
性質 |
含義 / 主要模型 |
前向序列Forward Sequence |
begin和end元函數能夠界定其頭尾範圍的類型序列 / MPL中所有序列 |
雙向序列Bidirectional Sequence |
迭代器屬於雙向迭代器的前向序列 / vector,range_c |
隨機訪問序列Random Access Sequence |
迭代器屬於隨機訪問迭代器的雙向序列 / vector,range_c |
可擴展序列Extensible Sequence |
允許插入和刪除元素的序列 / vector,list |
前可擴展序列Front Extensible Sequence |
允許在前端插入和刪除元素的可擴展序列 / vector,list |
後可擴展序列Back Extensible Sequence |
允許在後端插入和刪除元素的可擴展序列 / vector,list |
關聯序列Associative Sequence |
可以用key值來檢索元素的前向序列 / set,map |
可擴展關聯序列Extensible Associative Sequence |
允許插入和刪除元素的關聯序列 / set,map |
整型序列包裝器Integral Sequence Wrapper |
存放一系列整型常量類(Integral Constant)的一種類模板 / vector_c,list_c,set_c |
不定序列Variadic Sequence |
可以用給定元素個數或用不指定元素個數的形式來定義的序列 / vector,list,map |
部分概念在現階段的MPL版本中其實存在着一些冗餘,但這種以概念驅動的程序庫卻是很清晰的:每一種概念的背後都指明瞭它所支持的操作。
2.vector和deque
(1)概述
MPL中最簡單和最常用的序列就是vector。而deque在目前版本的MPL中相當於vector。vector的實質十分類似於前面示例的類型“數組”,邏輯上是連續線性的,由於它屬於不定序列,使用時既可以指定長度,以vectornn>來定義,也可以直接用vectorn>來定義。注意n不能超過宏BOOST_MPL_LIMIT_VECTOR_SIZE的定義,目前MPL的默認值是20。vector的特點是支持尾端常數時間的插入和刪除操作以及中段和前端線性時間的插入和刪除操作。
(2)操作
vector支持的操作無論在命名還是邏輯上基本都與STL 一致,但有一個重大區別,STL的操作函數定義在類的內部,但是限於模板元編程的特殊性,MPL的這些元函數在容器外定義。下表列出它們的用法:
begin::type
|
返回一個迭代器指向v的頭部 |
end::type
|
返回一個迭代器指向v的尾部 |
size::type
|
返回一個v的大小 |
empty::type
|
當且僅當v爲空時返回一個整型常量類,其值爲true |
front::type
|
返回v的第一個元素 |
back::type
|
返回v的最後一個元素 |
at::type
|
返回v的第n個元素 |
insert::type
|
返回一個新的vector使其定義爲[begin::type, pos), x, [pos, end::type) |
insert_range::type
|
返回一個新的vector使其定義爲[begin::type, pos), [begin::type, end::type) [pos, end::type) |
erase::type
|
返回一個新的vector使其定義爲[begin::type, pos), [next::type, end::type) |
erase::type
|
返回一個新的vector使其定義爲[begin::type, pos), [last, end::type) |
clear::type
|
返回一個空的vector |
push_back::type
|
返回一個新的vector使其定義爲[begin::type, end::type), x |
pop_back::type
|
返回一個新的vector使其定義爲[begin::type, prior< end::type >::type) |
push_front::type
|
返回一個新的vector使其定義爲[begin::type, end::type), x |
pop_front::type |
返回一個新的vector使其定義爲[next< begin::type >::type, end::type) |
(3)源代碼分析
MPL的源代碼有着比較複雜的脈絡,主要原因是爲了保持移植性,需要針對不同的編譯器問題進行規避。比如vector的底層就有三個不同的版本,第一個專門針對不支持模板局部特化的編譯器,第二個用於基於類型的序列,第三個是普通版本。在預處理時會根據情況確定使用哪一個版本。它們之間的差異是什麼呢?vector0的實現代碼中把它們放在了一起,正好可以說明其區別:
定義的上半部分是基於類型的版本,下半部分則用於另外兩個版本。MPL的參考手冊沒有說明vector的底層是實現的原理,看起來兩種實現之間的差異比較大,其中最重要的差別是vector_tag的用法。vector_tag同樣是一個底層的定義,作用應該是傳遞給各類算法,以區別不同的序列類型。tag的定義同樣有兩種:
大概基於類型的版本可以不必實例化一個vector_tag,性能上更優越。從MPL的config配置情況來看,似乎默認只使用基於類型的序列,也就是序列會以v_item作爲基類。限於篇幅,這裏僅分析基於類型的vector,下文有類似情況時也做同樣的處理,不一一展開了。
前面已經指出,vector是一個不定序列,這類序列可以不必指定參數的個數直接使用。C++模板支持不定個數的參數表嗎?當然不是。實際上不定序列的效果是通過模板的局部特化來實現的。而能夠確定個數的vectorn則是vector的基礎。因此首先要看看vectorn(n不等於0時)是怎樣實現的:
目前MPL對vector中元素個數的限制是20個以內,所以這段代碼一直遞推到vector20爲止。其中的v_item是一個最底層的結構,它包含的內容類似於上面vector0中的那些成員:
很容易聯想到前一部分提到的Typelist的Head和Tail結構,但是這裏並不像Typelist一樣需要一個NullType作結束標記。
至於不定序列vector的定義,則這樣給出:
顯然可以看出,參數個數之所以可以不定,只是一個特化後的假象而已,針對每一個n,vector都會繼承vectorn來製造這種假象。上面代碼中的na是一個特殊的類,專用於標明參數未使用。
順道一提deque,其實現也是通過繼承vectorn來實現的,比如:
所以說deque是與vector等價的一個概念。
3.list
(1)概述
MPL的list的原理類似於STL中list,其特點是支持前端常數時間的插入和刪除操作以及中段和尾端線性時間的插入和刪除操作。
(2)操作
list所支持的操作與vector完全一樣,參見vector的操作列表。
(3)源代碼分析
MPL的list的原理上十分類似於上面提到Typelist,其底層的結構l_item是這樣定義的:
另外還需要一個結束標記l_end:
可以看到,l_item接受三個參數,第一個參數表示list的長度,第二個參數相當於上文Typelist實現中的Head,第三個參數相當於Tail。由於遞推式的繼承關係,listn的終結標記總是爲l_end。
list作爲不定序列,其實現方式與vector如出一轍,也是通過繼承listn,比如對於一個參數的list:
4.set
(1)概述和操作
set保證了key值在序列中沒有重複,對它的插入和刪除操作都只需要常數時間。
較之於vector和list,set沒有pop_back,、pop_front,、push_front、push_back、insert_range、back等幾個操作,但另有幾個特殊的操作:
has_key<s,k>::type |
如果s中包含一個類型key值爲k,則返回一個整型常量類,其值爲true。 |
count<s,k>::type |
返回s中key值爲k的元素的序號。 |
order<s,k>::type |
返回s中key值爲k的元素唯一的整型常量類,其值是一個無符號整數。 |
at<s,k>::type at<s,k,def>::type |
返回s中含有key值爲k的元素。 |
key_type<s,x>::type |
返回類型等同於x。 |
value_type<s,x>::type |
返回類型等同於x。 |
erase_key<s,k>::type |
返回一個新的set,當中不包括key值k。 |
(2)源代碼分析
MPL的序列都有一個共同點,就是都從一個sequence0開始構造,並以x_item作爲存放類型的基礎結構。set比起上面的兩種序列都來得複雜,它的構造首先從set0開始: