MPL
元編程庫
MPL(Meta-Programming Library)是由David Abrahams和Aleksey Gurtovoy為方便模板元編程而開發的庫,2003年被Boost吸納為其中的一員,此後又歷經一些大幅度修改,已經相當完善,其最新版本於2004年11月發布。MPL的出現是C++模板元編程發展中的一大創舉,它提供了一個通用、高層次的編程框架,其中包括了序列(Sequence)、迭代器(Iterator)、演演算法(Algorithm)、元函數(Metafunction)等多種組件,具有高度的可重用性,不但提高了模板元編程的效率,而且使模板元編程的應用範圍得到相當的擴展。
一個庫的組織形式有時候甚至比它的功能還重要。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是一個高層次的庫,它的地位和編譯期執行的特殊性決定了它需要一些特殊的輔助設施,並對其他庫會有所依賴。
1.Boost的Preprocessor庫
Preprocessor庫是一個基於宏的元編程庫。預處理器的作用發生在編譯以前,所以它比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庫用於驗證傳遞的參數或參數之間是否符合一定的條件,比如可以判定兩個參數是否有繼承關係、是否可轉換等。
3.Boost的Static Assert庫
Static Assert庫用於編譯時斷言,用法類似於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等等,採取相應的補救措施。
序列是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版本中其實存在著一些冗餘,但這種以概念驅動的程序庫卻是很清晰的:每一種概念的背後都指明了它所支持的操作。
(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作為基類。
(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:
(1)概述和操作
set保證了key值在序列中沒有重複,對它的插入和刪除操作都只需要常數時間。
較之於vector和list,set沒有pop_back,、pop_front,、push_front、push_back、insert_range、back等幾個操作,但另有幾個特殊的操作:
has_key | 如果s中包含一個類型key值為k,則返回一個整型常量類,其值為true。 |
count | 返回s中key值為k的元素的序號。 |
order | 返回s中key值為k的元素唯一的整型常量類,其值是一個無符號整數。 |
at at | 返回s中含有key值為k的元素。 |
key_type | 返回類型等同於x。 |
value_type | 返回類型等同於x。 |
erase_key | 返回一個新的set,當中不包括key值k。 |
(2)源代碼分析
MPL的序列都有一個共同點,就是都從一個sequence0開始構造,並以x_item作為存放類型的基礎結構。
set比起上面的兩種序列都來得複雜,它的構造首先從set0開始。