THE BOOST C++ METAPROGRAMMING
LIBRARY
Aleksey Gurtovoy
MetaCommunications
David Abrahams
Boost Consulting
?
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
};
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等于0和N不等于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];
};
这里我们引入了“类型计算”的概念
像上面的factorial,permutation_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++给我们的编译期类型内省的能力(类似Java的Reflection)
没有办法找出成员的名字是什么,即使我们假定名字是按照上面的惯用法给出的,我们也没有办法知道有多少个成员;解决这个问题的关键是提高表示的一致性;如果我们有一个方法可以得到任何序列的第一个类型,和剩余的序列,那么我们将轻易的获取所有成员:
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++标准库的实现给出的容器和算法,都要比大量存在的特定工程的实现给出的容器和算法更加灵活,实现的更好,因为库的开发是以它本身为目的的,而不是作为其他应用附带的任务,
对任何给定功能的集中的实现,更容易应用优化和改进
?复用
比任何库都提供的代码的复用更重要,一个设计良好的泛型库建立了一个concepts和idioms的framework,这些concepts和idioms建立了解决问题的可复用的思维的模型;类似于C++ STL给了我们iterator concepts和function object protocol,Boost MPL提供了type-iterators 和 metafunction class protocol;一个考虑周全的idioms的framework节省了元程序员考虑与手头工作不相关的实现细节的时间,而让他们集中精力在手头问题上
?可移植性
一个优秀的库可以消除,掩盖丑陋的平台差异;理论上一个元程序库是完全泛型的,不需考虑这些问题,但实际上对模板的支持依然有大量的不一致,即使是标准化四年之后;这或许不应该感到惊奇:C++模板是语言最深远,最复杂的特性,极大的增强了C++元编程的能力
?有趣
一遍遍的重复同样的idioms是单调乏味的;它使程序员疲劳,降低生产力;更进一步,当程序员感到厌倦时他们会以比慢慢写代码更大的代价写出晦涩的,充满虫子的代码;通常最有用的库是机敏的程序员从海量的重复中提取出的简单的模式;MPL通过消除大量样板代码的重复来帮助程序员减少厌倦;
?
像人们能够看到的,MPL的开发是由推动了其它库的开发的同样的,实际的,真实世界中的问题推动的;或许这是一种迹象,表明模板元编程已经完成了最后的准备,即将离开深奥的领域,进入每个日常程序员的口头话题