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中的序列

概述

序列是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版本中其實存在著一些冗餘,但這種以概念驅動的程式庫卻是很清晰的:每一種概念的背後都指明了它所支持的操作。

vector和deque

(1)概述

MPL中最簡單和最常用的序列就是vector。而deque在目前版本的MPL中相當於vector。vector的實質十分類似於前面示例的類型“數組”,邏輯上是連續線性的,由於它屬於不定序列,使用時既可以指定長度,以vector nn>來定義,也可以直接用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作為基類。

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:

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開始。

相關詞條

熱門詞條

聯絡我們