《C++ Template Metaprogramming》
第三章 深度探索元函数
david abraham&Aleksey Gurtovoy著
刘未鹏(pp_liu@msn.com) 译
3.2 高阶元函数(Higher-Order Metafunctions)
在前面一节,我们传递或返回元函数时使用了两种格式——元函数类和占位符表达式。通过把元函数“塞进”first class的元数据中,能够允许transform执行各种不同的操作,例如,上面例子中的单位乘除。尽管“使用函数去操纵其它函数”的思想可能看起来比较简单,然而却具有非常强大的能力和灵活性,因此赢得了一个好听的名字:高阶函数式编程(higher-order functional programming)。操纵其它函数的函数被称为高阶函数。所以,transform是个高阶元函数:操纵其它元函数的元函数。
现在我们已经见识过了高阶元函数的强大能力,下面我们将尝试创建新的高阶元函数。为了探究其底层机理,让我们先来看一个简单的例子。我们的任务是写一个名为twice的元函数,twice满足下面的条件:给它一个一元元函数f以及任意的元数据x,它将作如下的计算:
twice(f,x) := f(f(x))
这个例子看起来没什么价值——它的确没有。你大概不会在实际编码中使用twice。但使用它并非我们的目的,twice包含了一个“高阶元函数”的所有必要元素,并且没有会令你分散注意力的其它细节,尽管它只是接受并调用一个元函数。
如果f是个元函数类,那么twice的定义会很直观:
template <class F, class X>
struct twice
{
typedef typename F::template apply<X>::type once; //f(x)
typedef typename F::template apply<once>::type type;//f(f(x))
};
或者使用元函数转发:
template <class F, class X>
struct twice
: F::template apply<
typename F::template apply<X>::type
>
{};
C++语言.附注
C++标准要求:当我们使用依赖名字(dependent name)并且该名字指的是一个成员模板时,我们必须使用template关键字。F::apply不一定指的是个模板名字,其含义依赖于F。而F::template apply则确切的告诉编译器apply(应该)是个成员模板。关于template,附录B有更多信息。
显然,在每次使用元函数类的时候都在apply前加上template关键字是个负担,通过将这种使用模式分解到一个元函数中,我们可以减轻这个负担:
template <class UnaryMetaFunctionClass, class Arg>
struct apply1
: UnaryMetaFunctionClass::template apply<Arg>
{};
现在,twice可以简化成这样:
template <class F, class X>
struct twice
: apply1<F, typename apply1<F,X>::type>
{};
我们来看一下twice的使用——将它应用到add_pointer_f元函数类上:
struct add_pointer_f
{
template <class T>
struct apply : boost::add_pointer<T> {};
};
BOOST_STATIC_ASSERT((
boost::is_same<
twice<add_pointer_f, int>::type
, int**
>::value
));
我们可以看出,将twice和add_pointer_f一起使用可以创建“指针的指针”。
3.3 处理占位符
虽然我们的twice实现已经可以与元函数类一起工作了,但理想情况下,我们还要求它能够与占位符表达式一起工作,就像transform允许我们传递两种形式的元函数一样。例如,我们得能够写出这样的代码:
template <class X>
struct two_pointers
: twice<boost::add_pointer<_1>, X>
{};
但是我们只要考察一下boost::add_pointer的实现就会发现,目前的twice根本不能这样工作:
template<class T>
struct add_pointer
{
typedef T* type;
}
boost::add_pointer<_1>必须是个元函数类(就像add_pointer_f那样),才能够被twice调用。然而事实上它却是一个无参(nullary)元函数,返回几乎毫无意义的_1*类型。所有试图使用two_pointers的地方都会失败,因为当apply1要求boost::add_pointer<_1>内嵌的::apply元函数时会发现其根本不存在。
我们并没有得到想要的行为。下面该怎么办呢?想想看,既然mpl::transform可以做到,那么我们应该也有办法做到——下面就是:
3.3.1 lambda元函数
我们可以使用MPL的lambda元函数,由boost::add_pointer<_1>生成一个元函数类:
template <class X>
struct two_pointers
: twice<typename mpl::lambda<boost::add_pointer<_1> >::type, X>
{};
BOOST_STATIC_ASSERT((
boost::is_same<
typename two_pointers<int>::type
, int**
>::value
));
后面我们将把add_pointer_f这样的元函数类或boost::add_pointer<_1>这样的占位符表达式统称lambda表达式。这个称呼的含义是“匿名(unnamed)函数对象”,它是在二十世纪三十年代由逻辑学家Alonzo Church引入的,作为被他称为lambda计算(lambda-calculus[1])的计算理论中的一部分。之所以使用lambda这个含义有点晦涩的名词是由于它在函数式编程语言中建立的良好先例。
尽管mpl::lambda的主要意图是将占位符表达式转化为元函数类,然而它也可以接受任何lambda表达式,即使该表达式已经是个元函数类。在后一种情况,mpl::lambda原样返回其参数。MPL算法(如transform)在内部使用了mpl::lambda,然后再调用其返回(生成)的元函数类,所以它们和两种lambda表达式都相处得不错。我们可以将相同的策略应用到twice上:
template <class F, class X>
struct twice
: apply1<
typename mpl::lambda<F>::type
, typename apply1<
typename mpl::lambda<F>::type
, X
>::type
>
{};
现在我们可以将twice和元函数类或占位符表达式一起使用了:
int* x;
twice<add_pointer_f, int>::type p = &x;
twice<boost::add_pointer<_1>, int>::type q = &x;
3.3.2 apply元函数
调用lambda返回的元函数类在MPL中是极为常见的模式,以至于MPL提供了一个apply元函数来做这件事。使用mpl::apply,我们的twice会变得更加灵活:
#include <boost/mpl/apply.hpp>
template <class F, class X>
struct twice
: mpl::apply<F, typename mpl::apply<F,X>::type>
{};
你可以将mpl::apply看作与apply1相同,不过apply有另外两个特性:
1.apply1只能操作元函数类,而mpl::apply的第一个参数可以是任意的lambda表达式(包括占位符表达式)[2]。
2.apply1只能接受除元函数类之外的1个额外参数,并将这个参数传给元函数类。而mpl::apply可以接受1至5个额外的参数[3],并用它们来调用元函数类。例如:
//将二元的lambda表达式应用到另外两个参数上
mpl::apply<
mpl::plus<_1,_2>
,mpl::int_<6>
,mpl::int_<7>
>::type::value // == 13
原则
如果你要在你的元函数中调用其某个参数(即:将某个参数作为元函数类来调用——译注),请使用mpl::apply以确保该调用对于两种lambda表达式皆是有效的。
3.4 lambda的其它能力
lambda表达式的能力并不止于使元函数成为可传递的参数。下面介绍的另外两种能力使lambda表达式成为几乎每个元编程任务中不可或缺的部分。
3.4.1 部分函数应用(Partial Metafunction Application)
考虑lambda表达式mpl::plus<_1,_1> :单个的参数会被传递到plus的两个“_1”的位置,也就是说,将一个值与自身相加。因此,这里,一个二元的元函数被用来创建了一个一元的lambda表达式。换句话说,我们创建了一个全新的运算(plus原先是做加法运算的,但plus<_1,_1>却是将一个值与自身相加,也就是“乘2”运算——译注)!然而,还不止这些,通过将一个普通类型(非占位符)绑定到plus的其中一个参数,我们可以创建一个一元lambda表达式,其作用为将它的参数加上一个定值(如42):
mpl::plus<_1,mpl::int_<42> >
将一集实参绑定到某个函数的形参的一个子集的过程在函数式编程语言[4]中被称为部分函数应用。
3.4.2 复合元函数[5](Metafunction Composition)
lambda表达式也可以被用于组合简单的元函数以产生更为有趣的运算。例如,下面的表达式将两个数的和与差相乘(即(a+b)*(a-b)——译注):
mpl::multiplies<mpl::plus<_1,_2>, mpl::minus<_1,_2> >
可以看出,它是三个元函数(multiplies,plus,minus)的复合体。
当对一个lambda表达式求值时,MPL会先检查它的各个参数以确定它们自身是否lambda表达式[6],如果是,则先将它们求值,并将这些(本身为lambda表达式的)参数替换为求值的结果,然后再对外围的lambda表达式求值[7]。
[1] http://en.wikipedia.org/wiki/Lambda_calculus
[2] 译注:不过似乎boost 1.31.0里面的mpl::apply并没有这个特性。或许是权衡后的考虑?
[3] MPL参考手册的Configuration Macros部分描述了如何改变mpl::apply能够接受的参数个数的上限。
[4] 译注:这里作者的原文是“...in the world of functional programming...”,本该译为“...在函数式编程中...”,然而考虑到“函数式编程”可能会发生误导,而译为“在函数式编程语言中”则不会,因为后者是个被广泛使用的名词。
[5] 译注:这里还可以译为“元函数组合”“元函数合成”等,视composition的译法而定。但考虑到数学中的“复合函数”一说,所以这里译为“复合元函数”,“复合”可作动词,可作形容词。如果作动词则表示“将元函数复合起来”,这正是原文表达的意思,如果作形容词则表示“复合后的元函数”,这是“复合”的结果。这样似乎更好一些:)。
[6] 译注:这里所说的lambda表达式的各个参数并非该lambda接受的“外界”参数,举个例子:mul<plus<_1,_2>,minus<_1,_2> >这个lambda表达式的参数就是 plus<_1,_2>和minus<_1,_2>而它们各自又都是lambda表达式,所以它们会先被求值,然后将结果传给mul。
[7] 译注:事实上,lambda表达式的求值是个递归的过程。