《C++ Template Metaprogramming》
第三章 深度探索元函数 (3)
david abraham&Aleksey Gurtovoy著
刘未鹏(pp_liu@msn.com) 译
3.5 Lambda的细节
现在你对MPL的lambda设施的语义应该有了一个大致的了解,既然如此,让我们将前面的理解形式化(正式化),并且考察一些更为深入的东西。
3.5.1 占位符
“占位符”的定义可能会吓你一跳:
定义
占位符是一个形为mpl::arg<N>的元函数。
3.5.1.1 实现(Implementation)
_1,_2,_3这些名字只不过是为了方便起见,其实它们是mpl::arg的特化版本的typedefs,mpl::arg<N>作为元函数的作用是选出(并返回)它的第N个参数[1]。占位符的实现像这样:
namespace boost {
namespace mpl {
namespace placeholders {
template <int N> struct arg; // 前导声明
struct void_;
template <>
struct arg<1>
{
template <
class A1, class A2 = void_, ... class Am = void_>
struct apply
{
typedef A1 type; // 返回其第一个参数
};
};
typedef arg<1> _1;
template <>
struct arg<2>
{
template <
class A1, class A2, class A3 = void_, ...class Am = void_
>
struct apply
{
typedef A2 type; //返回其第二个参数
};
};
typedef arg<2> _2;
//其它特化版本和typedefs...
}}}
前面说过,调用元函数类就是调用其内嵌的::apply元函数。当一个lambda表达式中的某个占位符被求值时,其实就是以该lambda表达式的实际参数来调用该占位符,然后该占位符会返回参数中的某一个[2]。再然后求值(返回)的结果会替换lambda表达式中该占位符所“占”的位置。如此重复,直到所有的占位符都被替换成它们所表示的(实际的)参数。
3.5.1.2 匿名(Unnamed)占位符
匿名占位符是个非常特殊的占位符,其定义如下:
namespace boost { namespace mpl { namespace placeholders {
typedef arg<-1> _; //匿名占位符
}}}
其实现细节并不重要。对于匿名占位符,你所需知道的就是:它是被特殊对待的。当一个lambda表达式被mpl::lambda转化为元函数类时,在某个给定的模板特化体中的第N个出现的匿名占位符会被替换为_N。
例如,下面的表3.1中的每一行都包含两个等价的lambda表达式:
表 3.1
mpl::plus<_,_>
mpl::plus<_1,_2>
boost::is_same<
_
,boost::add_pointer<_>
>
boost::is_same<
_1
,boost::add_pointer<_1>
>
mpl::multiplies<
mpl::plus<_,_>
,mpl::minus<_,_>
>
mpl::multiplies<
mpl::plus<_1,_2>
,mpl::minus<_1,_2>
>
3.5.2 占位符表达式的定义
现在你应该已经知道了占位符的含义了。既然如此,我们可以定义占位符表达式如下:
定义
一个占位符表达式是:
一个占位符
或者
一个其参数至少有一个为占位符表达式的模板特化体。
换句话说,一个占位符表达式始终包含(至少)一个占位符。
3.5.3 lambda和非元函数(Non-Metafunction)模板
关于占位符表达式,一个尚未讨论的细节是:为了使普通模板更容易融入元编程,MPL对它们使用了特殊的规则。在所有的占位符都被相应的实际参数替换后,如果作为结果的模板特化体X并没有内嵌的::type,那么结果就是X自身。
例如,mpl::apply<std::vector<_>,T>的结果始终都是std::vector<T>。如果不是由于这个行为,我们就得写一个元函数用于在lambda表达式中创建模板特化体:
// trivial std::vector generator
template<class U>
struct make_vector { typedef std::vector<U> type; };
typedef mpl::apply<make_vector<_>, T>::type vector_of_t;
但是现在由于有了这个特殊规则,我们可以简单地写:
typedef mpl::apply<std::vector<_>, T>::type vector_of_t;
3.5.4 “懒惰”的重要性
回顾上一章提到的always_int:
struct always_int
{
typedef int type;
};
无参(nullary)元函数可能看起来并不重要,因为像add_pointer<int>这样的类型在任何lambda表达式中出现的地方都可以被替换为int*。但并非所有的无参元函数都像这样简单!例如:
struct add_pointer_f
{
template <class T>
struct apply : boost::add_pointer<T> {};
};
typedef mpl::vector<int, char*, double&> seq;
typedef mpl::transform<seq, add_pointer_f> calc_ptr_seq;
注意到calc_ptr_seq是个无参元函数,因为它有transform的内嵌::type。但是,对于一个C++模板,只有当我们试图“观察其内部”时,它才会被实例化。仅仅将calc_ptr_seq作为一个typedef名字并不会导致它被求值,因为我们并没有访问它内部的::type。
元函数接受了它的参数后仍可以被延迟调用。当一个元函数只是被选择性的使用时,我们可以使用惰性求值[3](lazy evaluation)来减少编译时间。有时,通过命名[4]一个无效的计算而并不去实际执行它,我们还可以避免扭曲程序结构[5]。我们对calc_ptr_seq正是这么做的,因为double&*是非法类型。这种“懒性”和它的优点是本书中将会重复出现的主题。
3.6 细节
到目前为止,你对一般的模板元编程和Boost的MPL库的基本概念和语言应该有了一个相当全面的了解。本节回顾其中的要点。
元函数转发(Metafunction forwarding)
使用public继承将元函数中内嵌的::type暴露给用户的技术[6]。
元函数类(Metafunction class)
将编译期函数形式化的最基本方法,由此,编译期函数可以被看作多态的元数据,也就是看作一个类型。元函数类是个内嵌有名为apply的元函数的类。
MPL
本书中的大部分例子都用到了Boost Metaprogramming Library(即MPL)。正如Boost的type traits的头文件一样,MPL头文件遵循一个简单的约定:
#include <boost/mpl/component-name.hpp>
然而,如果MPL的某个组件名以下划线结尾,那么对应的MPL头文件名就不包含最后的下划线。例如,mpl::bool_可以在<boost/mpl/bool.hpp>中找到。如果该库的哪些地方没有遵循这个约定,我们会为你指出来。
高阶函数(Higher-order function)
操作或返回函数的函数。利用其它元数据使元函数成为多态的是高阶元编程中的一个关键之处。
lambda表达式
简单的说,lambda表达式是可以被调用的元数据。如果没有可调用元数据的某些形式,高阶元函数也不会成为可能。lambda表达式有两个基本形式:元函数类和占位符表达式。
占位符表达式
lambda表达式的一种。通过使用占位符达到部分函数应用和复合元函数的目的。正如你将会在本书中随处可见的,这些特性给予我们惊人的能力,允许我们从原始的元函数构造出几乎任意复杂的类型计算——就在它被使用之处:
// find the position of a type x in some_sequence such that:
// x is convertible to 'int'
// && x is not 'char'
// && x is not a floating type
typedef mpl::find_if<
some_sequence
, mpl::and_<
boost::is_convertible<_1,int>
, mpl::not_<boost::is_same<_1,char> >
, mpl::not_<boost::is_float<_1> >
>
>::type iter;
占位符表达式使我们不必(为元函数)写新的(外覆)元函数类,实现了算法复用的目的。而这种能力在STL的运行期世界里却严重缺乏,因为如果不论标准算法的正确性和效率,则手写一个循环往往比使用标准算法简单得多。
lambda元函数(The ‘lambda’ metafunction)
将lambda表达式转化为元函数类的元函数。要得到关于lambda和lambda求值过程的更为详细的信息,请参考MPL的参考手册。
apply元函数(The ‘apply’ metafunction)
一个元函数,其行为是:以其余的参数去调用其第一个参数,后者必须是个lambda表达式。通常,要调用一个lambda表达式,你应该总是将它以及调用它的参数传给mpl::apply,而不是“手动”使用mpl::lambda。
惰性求值(Lazy evaluation)
一种将计算推迟到其结果被要求的时候的策略。这种策略可以避免所有不必要的计算和不必要的错误。元函数仅仅在我们访问其内嵌的::type时才会被(真正)调用,所以我们可以在提供了其所有参数的同时却不作任何实质性的计算,而是将求值延迟到必要的时候。
[1] MPL缺省提供了5个占位符。MPL参考手册的Configuration Macros部分有关于如何改变提供的占位符的数目的描述。
[2] 译注:如果该占位符为_N,那么就会返回实际参数中的第N个参数,占位符的“占位”的意思就是:_N“占”的是第N个参数的位置。
[3] 译注:lazy evaluation的意思是“不到必要时不求值”。
[4] 译注:这里,“命名(naming)”的意思是,仅仅给它一个名字(意味着“仅仅实例化它的名字”),而并不对该计算求值(意味着“并不实例化该类”)。
[5] 译注:一个不错的例子是apply_if,其详细介绍见boost的官方文档。
[6] 译注:这里的原文写得相当拗口,所以译文遵循前文的定义。含义一样。