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的:
// Preprocessor的宏,得到目前属于第几次迭代 #define i_ BOOST_PP_FRAME_ITERATION(1) ... template< //Preprocessor的宏,枚举参数列表 BOOST_PP_ENUM_PARAMS(i_, typename T) > // Preprocessor的宏,拼合vector和当前次数 struct BOOST_PP_CAT(vector,i_) { ... }
然后为了迭代n个上面的类模板,另一个文件则需要重复include这个文件,利用Preprocessor的文件迭代能力可以这样写:
// Preprocessor的宏,其中第一个参数3表示后面的参数组有3个 //元素,0和10表示迭代的范围是从0到10,最后一个参数是文件 //迭代的文件名 #define BOOST_PP_ITERATION_PARAMS_1 / (3,(0, 10, )) // Preprocessor的宏,要求按照上面的指定的参数进行递归 #include BOOST_PP_ITERATE()
尽管如此,宏还是必需的,它不但避免了重复编写递推式的代码(比如在上述的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的实现代码中把它们放在了一起,正好可以说明其区别:
template< typename Dummy = na > struct vector0; template<> struct vector0 { #if defined(BOOST_MPL_CFG_TYPEOF_BASED_SEQUENCES) typedef aux::vector_tag tag; typedef vector0 type; typedef long_<32768> lower_bound_; typedef lower_bound_ upper_bound_; typedef long_<0> size; static aux::type_wrapper item_(...); #else typedef aux::vector_tag<0> tag; typedef vector0 type; typedef void_ item0; typedef v_iter,0> begin; typedef v_iter,0> end; #endif };
定义的上半部分是基于类型的版本,下半部分则用于另外两个版本。MPL的参考手册没有说明vector的底层是实现的原理,看起来两种实现之间的差异比较大,其中最重要的差别是vector_tag的用法。vector_tag同样是一个底层的定义,作用应该是传递给各类算法,以区别不同的序列类型。tag的定义同样有两种:
#if defined(BOOST_MPL_CFG_TYPEOF_BASED_SEQUENCES) struct vector_tag; #else template< long N > struct vector_tag; #endif
大概基于类型的版本可以不必实例化一个vector_tag,性能上更优越。从MPL的config配置情况来看,似乎默认只使用基于类型的序列,也就是序列会以v_item作为基类。限于篇幅,这里仅分析基于类型的vector,下文有类似情况时也做同样的处理,不一一展开了。
前面已经指出,vector是一个不定序列,这类序列可以不必指定参数的个数直接使用。C++模板支持不定个数的参数表吗?当然不是。实际上不定序列的效果是通过模板的局部特化来实现的。而能够确定个数的vectorn则是vector的基础。因此首先要看看vectorn(n不等于0时)是怎样实现的:
template< typename T0 > struct vector1: v_item< T0, vector0<> > { typedef vector1 type; }; template< typename T0, typename T1 > struct vector2: v_item< T1, vector1 > { typedef vector2 type; };
目前MPL对vector中元素个数的限制是20个以内,所以这段代码一直递推到vector20为止。其中的v_item是一个最底层的结构,它包含的内容类似于上面vector0中的那些成员:
template< typename T, typename Base, int at_front = 0 > struct v_item: Base { typedef typename Base::upper_bound_ index_; typedef typename next::type upper_bound_; typedef typename next::type size; typedef Base base; //这个空的静态函数将在at元函数中有确定类型位置的作用 static aux::type_wrapper item_(index_); //默认的继承方式是private,重新使之可见 using Base::item_; };
很容易联想到前一部分提到的Typelist的Head和Tail结构,但是这里并不像Typelist一样需要一个NullType作结束标记。
至于不定序列vector的定义,则这样给出:
template< typename T0 = na, typename T1 = na, typename T2 = na, typename T3 = na, typename T4 = na, typename T5 = na, typename T6 = na, typename T7 = na, typename T8 = na, typename T9 = na, typename T10 = na, typename T11 = na, typename T12 = na, typename T13 = na, typename T14 = na , typename T15 = na, typename T16 = na, typename T17 = na , typename T18 = na, typename T19 = na > struct vector; template<> struct vector< na, na, na, na, na, na, na, na, na, na, na, na, na, na, na, na, na, na, na, na >: vector0<> { typedef vector0< >::type type; }; template< typename T0 > struct vector< T0, na, na, na, na, na, na, na, na, na, na, na, na, na, na, na, na, na, na, na >: vector1 { typedef typename vector1::type type; };
显然可以看出,参数个数之所以可以不定,只是一个特化后的假象而已,针对每一个n,vector都会继承vectorn来制造这种假象。上面代码中的na是一个特殊的类,专用于标明参数未使用。
顺道一提deque,其实现也是通过继承vectorn来实现的,比如:
template< typename T0 > struct deque< T0, na, na, na, na, na, na, na, na, na, na, na, na, na, na, na, na, na, na, na >: vector1 { typedef typename vector1::type type; };
所以说deque是与vector等价的一个概念。
3.list
(1)概述
MPL的list的原理类似于STL中list,其特点是支持前端常数时间的插入和删除操作以及中段和尾端线性时间的插入和删除操作。
(2)操作
list所支持的操作与vector完全一样,参见vector的操作列表。
(3)源代码分析
MPL的list的原理上十分类似于上面提到Typelist,其底层的结构l_item是这样定义的:
template<typename Size, typename T, typename Next> struct l_item { typedef aux::list_tag tag; typedef l_item type; typedef Size size; typedef T item; typedef Next next; };
另外还需要一个结束标记l_end:
struct l_end { typedef aux::list_tag tag; typedef l_end type; typedef long_<0> size; };
可以看到,l_item接受三个参数,第一个参数表示list的长度,第二个参数相当于上文Typelist实现中的Head,第三个参数相当于Tail。由于递推式的继承关系,listn的终结标记总是为l_end。
list作为不定序列,其实现方式与vector如出一辙,也是通过继承listn,比如对于一个参数的list:
template<typename T0> struct list< T0, na, na, na, na, na, na, na, na, na, na, na, na, na, na, na, na, na, na, na >: list1<T0> { typedef typename list1<T0>::type type; };
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开始: