《C++ Template Metaprogramming》
第三章:深度探索元函数
By David Abraham
(http://www.boost.org/people/dave_abrahams.htm)
By 刘未鹏(pongba)
C++的罗浮宫(http://blog.csdn.net/pongba)
原文链接(http://www.boost-consulting.com/mplbook)
C++模板元编程是C++最高阶最抽象也是最强大的能力,也是最前卫的技术,其带来的复用性以及代码强大的表达能力令人瞠目结舌,本章深入讨论C++元编程的机理以及应用,展现C++中最高阶的语义...
有了前面的基础知识作铺垫,我们来考察模板元编程技术的一个最基本的应用——为传统的不进行类型检查的操作添加静态类型检查。为此,我们将考察一个有关工程科学的实例——几乎在所有涉及科学计算的代码中都可以找到它的应用。在考察该例子的过程中,你将会学到一些重要的新的concepts,并且尝试使用MPL[1](Metaprogramming Library)进行高阶的模板元编程。
3.1 单位[2]分析
物理计算的首要原则是:数值并非是独立的——大多数物理量都有单位。而我们一不小心就会将单位置之脑后,这是件很危险的事情。随着计算变得越来越复杂,维持物理量的正确单位能够避免诸如“将质量赋给长度”和“将加速度和速度相加”之类不经意间就会犯下的错误。这意味着为数值建立一个类型系统。
手动检查类型是件单调而乏味的工作,并且容易导致错误。当人们感到厌烦时,注意力就会分散,从而容易犯错误。然而,类型检查不正是计算机擅长的工作吗?如果我们能够为物理量和单位构建一个C++型别的framework,那么我们从公式中就可以捕获错误,而不用等到它们在现实世界中导致问题的时候。
阻止单位不同的物理量互操作并不难——我们可以简单地用类来表现单位,并且只允许相同的类(单位)互操作。但是问题远不止这么简单,不同的单位可以通过乘或除结合起来,从而产生一个复杂的新单位,由于可以不断乘除,所以产生的新单位其复杂度几乎是任意的。看来问题变得更有趣了!例如,牛顿定律(它将力,质量,加速度三者联系起来):
F=ma
由于质量和加速度有着不同的单位,所以力的单位必须是两者的结合。事实上,加速度的单位就已经是个“混合物”了——单位时间内速度的改变:
dv/dt
又因为速度即“单位时间内经过的距离”,所以加速度的基本单位是:
(l/t)/t=l/t2
并且,加速度通常以“米每平方秒”来衡量。所以,力的单位为:
ml/t2
也就说,力通常以kg(m/s2)或“千克米每平方秒”来衡量。当我们将质量和加速度相乘时,我们除了将数量相乘之外还必须将单位相乘,这可以帮我们确信结果是有意义的。这种(对单位的)簿记的正式名称为单位分析,而我们的下一个任务就是在C++类型系统中实现它。John Barton和Lee Nackman在它们的著作《Scientific and Engineering C++》中第一次展示了如何实现它。我们将沿袭他们的思路,只不过重新以元编程的方式来实现。
3.1.1 单位的表示
国际标准单位制规定了物理量的标准单位为:质量(kg),长度或位置(m),时间(s),电荷(c),温度(oc),密度(kg/m3),角度(o)。为了通用一些,我们的系统必须可以表示七个或七个以上的基本单位,还要能够表示复合单位,比如力(kg(m/s2))的单位这种经过几个基本单位乘除而成的复合单位。
一般来说,一个复合单位可以看成若干基本单位的幂的乘积[3]。如果要表示这些幂次以便在运行期可以操纵它们,我们可以使用一个数组,其七个元素每个对应一个不同的单位,而其值表示对应单位的幂次:
typedef int dimension[7]; //m l t ...
dimension const mass ={1,0,0,0,0,0,0};
dimension const length ={0,1,0,0,0,0,0};
dimension const time ={0,0,1,0,0,0,0};
...
根据这种表示法,力的表示如下:
dimension const force = {1,1,-2,0,0,0};
也就是说,mlt-2。然而,如果我们想要将单位融入到类型系统[4]中去,这些数组就无法胜任了:它们的类型全都相同,都是dimension!而我们需要的是自身能够表示数值序列的类型,这样质量和长度的类型就是不同的,而两个质量的类型则是相同的。
幸运的是,MPL提供了一组表示类型序列的设施。例如,我们可以构建一个有符号整型的序列:
#include <boost/mpl/vector.hpp>
typedef boost::mpl::vector<
signed char, short, int, long> signed_types;
那么,我们如何用类型序列来表示单位[5]呢?由于数值型的元函数传递和返回的类型是具有内嵌::value的外覆类,所以数值序列其实是外覆类型的序列(另一个多态的例子)。为了使事情变得更为简单,MPL提供了int_<N>类模板,它以一个内嵌的::value来表现它的整型参数N:
#include <boost/mpl/int.hpp>
namespace mpl = boost::mpl;[6] // namespace alias
static int const five = mpl::int_<5>::value;
事实上,MPL库包含了一整套整型常量的外覆类,如long_和bool_等,每个外覆类对应一个不同类型的整型常量。
现在,我们可以将基本单位构建如下:
typedef mpl::vector<
mpl::int_<1>, mpl::int_<0>, mpl::int_<0>, mpl::int_<0>
, mpl::int_<0>, mpl::int_<0>, mpl::int_<0>
> mass;
typedef mpl::vector<
mpl::int_<0>, mpl::int_<1>, mpl::int_<0>, mpl::int_<0>
, mpl::int_<0>, mpl::int_<0>, mpl::int_<0>
> length;
...
唔...你很快就会觉得这写起来实在太累人。更糟糕的是,这样的代码难于阅读和验证。代码的本质信息,也就是每个基本单位的幂次,被埋在重复的语法“噪音”中。因此,MPL相应还提供了整型序列外覆类,它允许我们写出类似下面的代码:
#include <boost/mpl/vector_c.hpp>
typedef mpl::vector_c<int,1,0,0,0,0,0,0> mass;
typedef mpl::vector_c<int,0,1,0,0,0,0,0> length; // or position
typedef mpl::vector_c<int,0,0,1,0,0,0,0> time;
typedef mpl::vector_c<int,0,0,0,1,0,0,0> charge;
typedef mpl::vector_c<int,0,0,0,0,1,0,0> temperature;
typedef mpl::vector_c<int,0,0,0,0,0,1,0> intensity;
typedef mpl::vector_c<int,0,0,0,0,0,0,1> angle;
你可以将这个特殊的mpl::vector_c看作与前面那个冗长的mpl::vector一样,尽管它们的类型并不相同。
如果我们愿意,我们还可以定义一些复合单位:
// 基本单位:m l t ...
typedef mpl::vector_c<int,0,1,-1,0,0,0,0> velocity; // l/t
typedef mpl::vector_c<int,0,1,-2,0,0,0,0> acceleration;
// l/(t2)
typedef mpl::vector_c<int,1,1,-1,0,0,0,0> momentum; // ml/t
typedef mpl::vector_c<int,1,1,-2,0,0,0,0> force; // ml/(t2)
并且,有时候,标量的单位(如pi,标量的单位即没有单位——译注)也可以这样来描述:
typedef mpl::vector_c<int,0,0,0,0,0,0,0> scalar;
3.1.2 物理量的表示
上面所列的类型仍然是纯粹的元数据。要想对真实的计算进行类型检查,我们还需要以某种方式将它们(元数据)绑定到运行时数据。一个简单的数值外覆类——模板参数为数据类型T和T的单位——刚好合适:
template <class T, class Dimensions>
struct quantity
{
explicit quantity(T x)
: m_value(x)
{}
T value() const { return m_value; }
private:
T m_value;
};
现在,我们有了将数值和单位联系到一起的办法。例如,我们可以说:
quantity<float,length> l( 1.0f );
quantity<float,mass> m( 2.0f );
注意到在quantity的类定义体中并没有出现Dimensions模板参数的任何身影,它只在模板参数列表中出现过,其唯一作用是确保l和m具有不同的类型。这样,我们就不可能错误地将长度赋给质量:
m = l; // 编译期错误
3.1.3 实现加法和减法
因为参数的类型(单位)必须总是匹配,所以我们现在可以轻易的写出加法和减法的规则:
template <class T, class D>
quantity<T,D>
operator+(quantity<T,D> x, quantity<T,D> y)
{
return quantity<T,D>(x.value() + y.value());
}
template <class T, class D>
quantity<T,D>
operator-(quantity<T,D> x, quantity<T,D> y)
{
return quantity<T,D>(x.value() - y.value());
}
这样,我们就可以写出类似下面的代码:
quantity<float,length> len1( 1.0f );
quantity<float,length> len2( 2.0f );
len1 = len1 + len2; //ok
并且,我们不能将不同单位的量相加:
len1 = len1 = quantity<float, mass>( 3.7f ); // error
3.1.4 实现乘法
乘法比加减法复杂一些。到目前为止,运算的参数和结果的单位都是一样的,但是做乘法时,结果的单位往往和两个参数的单位都不相同。对于乘法,下面的式子:
(Xa)(Xb) == X(a+b)
意味着结果的单位的指数为相应参数的单位的指数和。商与此类似,为指数差。
为此,我们使用MPL的transform算法来将两个序列中的对应元素相加。transform是个元函数,它遍历两个并行的输入序列,对于每个位置将两个序列中的对应元素传给一个任意的(用户提供的)二元元函数,并且将结果存入一个输出序列。
template <
class Sequence1,
class Sequence2,
class BinaryOperation
>
struct transform; // return a sequence
如果你熟悉STL的transform算法的话,上面的struct transform的形式对于你可能并不陌生,STL的transform算法接受两个运行期的输入序列:
template <
class InputIterator1, class InputIterator2
, class OutputIterator, class BinaryOperation
>
void transform(
InputIterator1 start1, InputIterator2 finish1
, InputIterator2 start2
, OutputIterator result, BinaryOperation func);
现在我们只需要向mpl::transform传递一个用于对单位进行乘除法(通过对两个序列的对应元素相加减)的BinaryOperation。如果你查看MPL的参考手册,你会看到plus和minus两个元函数刚好可以满足要求:
#include <boost/static_assert.hpp>
#include <boost/mpl/plus.hpp>
#include <boost/mpl/int.hpp>
namespace mpl = boost::mpl;
BOOST_STATIC_ASSERT((
mpl::plus<
mpl::int_<2>
, mpl::int_<3>
>::type::value == 5
));
BOOST_STATIC_ASSERT是一个宏,如果其参数为false,则会导致一个编译期错误。双括号是必要的,因为C++预处理器不能解析模板:如果不多加一对括号,那么它会将隔开模板参数的逗号当成隔开宏参数的逗号,从而将条件表达式错误地解析为若干宏参数。这和运行期的assert(...)不一样(后者是由C++编译期解析的,可以识别一切表达式——译注),BOOST_STATIC_ASSERT也可以用于类的定义域中,从而允许我们将其置于元函数中。第8章对此有更深入的讨论。
到目前为止,看起来我们已经有了一个解决方案,像这样:
#include <boost/mpl/transform.hpp>
template <class T, class D1, class D2>
quantity<
T
, typename mpl::transform<D1,D2,mpl::plus>::type
>
operator*(quantity<T,D1> x, quantity<T,D2> y) { ... }
但是很抱歉,这还不够!现在如果你试图使用这个operator*,你会得到一个编译错误,原因就在于你将mpl::plus直接传给了mpl::transform,而(MPL的)规定却说元函数的参数必须是类型,但mpl::plus却不是类型,而是一个类模板。所以我们必须通过某种方式让类似plus这样的元函数满足这种元数据(metadata)模型。
从某种意义上说,这就要求在元函数和元数据之间引入多态,一个很自然的途径是使用外覆类惯用手法——在前面的代码中,这种惯用手法曾在类型和整型常量之间引入了多态。而现在,我们将一个类模板内嵌于一个所谓的元函数类[7]中:
struct plus_f
{
template <class T1, class T2>
struct apply
{
typedef typename mpl::plus<T1,T2>::type type;
};
};
定义:元函数类是指内嵌有名为apply的public 元函数的类。
虽然元函数是模板而非类型,但是元函数类却以一个普通的非模板类将其包覆起来,使其成为一个类型。因为元函数操作和返回的都是类型,所以元函数类也可以被作为参数传递给另一个元函数,而元函数也可以返回一个元函数类。
从而,我们得到了一个plus_f元函数类,将它作为BinaryOperation传递给mpl::transform不会导致编译错误:
template <class T, class D1, class D2>
quantity<
T
, typename mpl::transform<D1,D2,plus_f>::type //new dimensions
>
operator*(quantity<T,D1> x, quantity<T,D2> y)
{
typedef typename mpl::transform<D1,D2,plus_f>::type dim;
return quantity<T,dim>( x.value() * y.value() );
}
现在,如果我们计算一个 5 公斤 的膝上型计算机的重力,也就是说,将重力加速度乘以质量:
quantity<float,mass> m( 5.0f );
quantity<float,acceleration> a( 9.8f );
std::cout << "force = " << (m * a).value();
我们自定义的operator*会将这些运行期的值相乘(结果为 49f ),而我们的元程序代码则会通过transform将表现基本单位的元序列进行指数相加,所以结果类型为一个新的单位,其表示像这样:
mpl::vector_c<int,1,1,-2,0,0,0,0> // kgms-2
然而,如果我们试图写:
quantity<float,force> f = m*a;
我们会遇到一点问题。尽管m*a的结果的确表示:质量,长度,时间的指数分别为1,1,-2,然而transform返回的类型却并非vector_c。相反,transform处理它的输入元素,并以恰当的元素创建一个新的序列:这个新序列和mpl::vector_c<int,1,1,-2,0,0,0,0>具有几乎相同的属性,但它们却是完全不同的C++类型。如果你想要知道新序列的全名,你可以尝试编译这个例子,然后查看错误信息,但是确切的细节并不重要。关键的问题是force的类型和新序列的类型不同,所以赋值会失败。
为了解决这个问题,我们可以添加一个从乘法的结果类型到quantity<float,force>的隐式转换。由于我们无法预测介入计算的单位的确切类型(从而也就无法预测计算的结果的单位——译注),所以这个转换必须为模板形式的,像这样:
template <class T, class Dimensions>
struct quantity
{
// converting constructor
template <class OtherDimensions>
quantity(quantity<T,OtherDimensions> const& rhs)
: m_value(rhs.value())
{
}
...
然而,很不幸的是,这样一个通用的转换彻底违背了我们原来的意图,一旦有了这个转换,我们就可以写出下面的代码:
// m*a的结果应该是力(force),而非质量(mass)!
quantity<float,mass> bogus = m * a;
这简直糟透了!
幸运的是,我们可以通过另一个MPL算法——equal——来解决这个问题,equal用于测试两个序列是否具有相同的一集元素:
template <class OtherDimensions>
quantity(quantity<T,OtherDimensions> const& rhs)
: m_value(rhs.value())
{
BOOST_STATIC_ASSERT((
mpl::equal<Dimensions,OtherDimensions>::type::value
));
}
现在,如果两个单位不匹配,那么这个assertion就会导致一个编译错误,及时阻止你的错误行为。
3.1.5 实现除法
除法和乘法类似,乘法将指数相加,而除法将指数相减。显然,作除法的元函数类minus_f完全可以按照plus_f的形式来写,但这里我们将使用一个新的技巧来进一步简化minus_f元函数类:
struct minus_f
{
template <class T1, class T2>
struct apply
: mpl::minus<T1,T2> {};
};
这里,minus_f::apply使用了继承来将其基类mpl::minus的“type”内嵌类型暴露出来。这样我们就不必写:
typedef typename ...::type type
这个强有力的简化代码的手法被称为元函数转发。后面我们还会频繁使用它。注意,我们不用在apply的基类mpl::minus<T1,T2>前面加上typename(加了反而会错),因为编译器知道apply的基类列表中只可能有类型。
尽管有这样的语法技巧来简化代码,但一遍遍地写这些简单之极的外覆类仍然会很快让人感到厌烦。虽然minus_f没有plus_f那么臃肿,但你仍要为此写一堆代码。幸运的是,MPL为我们提供了简单得多的办法,我们用不着写一整个的元函数类(如minus_f),而是可以“直接”将元函数传给算法,例如,我们可以这样调用mpl::transform:
typename mpl::transform<D1,D2,mpl::minus<_1, _2> >::type
其中有两个看起来很奇怪的参数(_1和_2),它们被称为占位符,这里它们的意思是:当transform的BinaryOperation被调用时,其第一第二个参数会被相应地传递到minus的_1和_2处。而mpl::minus<_1,_2>则被称为占位符表达式。
附注:MPL的占位符位于mpl::placeholders名字空间内,定义在boost/mpl/placeholder.hpp文件中。在本书中,我们会假定你已经写了如下代码:
#include<boost/mpl/placeholder.hpp>
using namespace mpl::placeholders;
这样,像_1,_2这样的占位符才能够不加名字空间限定的访问。
使用占位符后的operator / 像这样:
template <class T, class D1, class D2>
quantity<
T
, typename mpl::transform<D1,D2,mpl::minus<_1,_2> >::type
>
operator / (quantity<T,D1> x, quantity<T,D2> y)
{
typedef typename
mpl::transform<D1,D2,mpl::minus<_1,_2> >::type dim;
return quantity<T,dim>( x.value() / y.value() );
}
代码明显变得更为简洁了(因为用不着额外定义一个minus_f类)。我们还可以将计算新单位的代码分解到一个新的元函数中,这样代码将继续得到简化:
template <class D1, class D2>
struct divide_dimensions
: mpl::transform<D1,D2,mpl::minus<_1,_2> > //再次转发
{};
template <class T, class D1, class D2>
quantity<T, typename divide_dimensions<D1,D2>::type>
operator/(quantity<T,D1> x, quantity<T,D2> y)
{
return quantity<T, typename divide_dimensions<D1,D2>::type>(
x.value() / y.value());
}
现在我们可以验证膝上型计算机的重力是否计算正确,通过一个逆向的计算,我们得到其质量,然后将它与条件给出的计算机质量比较:
quantity<float,mass> m2 = f/a;
float rounding_error = std::abs((m2-m).value());
如果一切正常,那么rounding_error会非常接近0。这些计算虽令人厌烦,但是如果它们出错则往往会破坏整个程序(甚至更糟)。如果我们将f/a错写成了a/f,我们会得到一个编译错误,及时防止错误在整个程序中蔓延。
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[8])的计算理论中的一部分。之所以使用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 a pply元函数
调用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表达式(包括占位符表达式)[9]。
2. apply1只能接受除元函数类之外的1个额外参数,并将这个参数传给元函数类。而mpl::apply可以接受1至5个额外的参数[10],并用它们来调用元函数类。例如:
//将二元的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> >
将一集实参绑定到某个函数的形参的一个子集的过程在函数式编程语言[11]中被称为部分函数应用。
3.4.2 复合元函数[12](Metafunction Composition)
lambda表达式也可以被用于组合简单的元函数以产生更为有趣的运算。例如,下面的表达式将两个数的和与差相乘(即(a+b)*(a-b)——译注):
mpl::multiplies<mpl::plus<_1,_2>, mpl::minus<_1,_2> >
可以看出,它是三个元函数(multiplies,plus,minus)的复合体。
当对一个lambda表达式求值时,MPL会先检查它的各个参数以确定它们自身是否lambda表达式[13],如果是,则先将它们求值,并将这些(本身为lambda表达式的)参数替换为求值的结果,然后再对外围的lambda表达式求值[14]。
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个参数[15]。占位符的实现像这样:
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表达式的实际参数来调用该占位符,然后该占位符会返回参数中的某一个[16]。再然后求值(返回)的结果会替换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。
元函数接受了它的参数后仍可以被延迟调用。当一个元函数只是被选择性的使用时,我们可以使用惰性求值[17](lazy evaluation)来减少编译时间。有时,通过命名[18]一个无效的计算而并不去实际执行它,我们还可以避免扭曲程序结构[19]。我们对calc_ptr_seq正是这么做的,因为double&*是非法类型。这种“懒性”和它的优点是本书中将会重复出现的主题。
3.6 细节
到目前为止,你对一般的模板元编程和Boost的MPL库的基本概念和语言应该有了一个相当全面的了解。本节回顾其中的要点。
元函数转发(Metafunction forwarding)
使用public继承将元函数中内嵌的::type暴露给用户的技术[20]。
元函数类(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是Boost库里面的一个子库。用于支持模板元编程。下文会多次提到这个MPL库。
[2] 译注:这里原文为Dimensional Analysis,这里的Dimensional并非作通常意义上的“维度”解释。而是作为物理上的“单位”解释,因为下文讲的正是如何在编译期对物理量进行单位检查,进而实现一个编译期的健全的单位系统。Dimensional Analysis的正式称呼为“量纲分析”,太学术化,所以这里我们用通常物理上的称呼。
[3] 将1/x看成x的-1次方。由此,m/s2可以写成ms-2,就由商的形式变成了积的形式。
[4] 译注:作者的意思是“让每个不同的单位成为不同的类型”。
[5] 译注:这里的原文是“...represent numbers”,直译为“...表示数值”,但这里的意思其实是表示数值的单位。
[6] namespace alias=namespace-name;将alias声明为namespace-name的别名。在本书的许多例子中都会使用mpl::来表示boost::mpl::。
[7] 译注:元函数本身是个类模板。而元函数类是个类型,它将元函数内嵌为一个名为apply的类模板,这两个称呼在后面将会多次提到,请读者注意它们的区别。
[9] 译注:不过似乎boost 1.31.0 里面的mpl::apply并没有这个特性。或许是权衡后的考虑?
[10] MPL参考手册的Configuration Macros部分描述了如何改变mpl::apply能够接受的参数个数的上限。
[11] 译注:这里作者的原文是“...in the world of functional programming...”,本该译为“...在函数式编程中...”,然而考虑到“函数式编程”可能会发生误导,而译为“在函数式编程语言中”则不会,因为后者是个被广泛使用的名词。
[12] 译注:这里还可以译为“元函数组合”“元函数合成”等,视composition的译法而定。但考虑到数学中的“复合函数”一说,所以这里译为“复合元函数”,“复合”可作动词,可作形容词。如果作动词则表示“将元函数复合起来”,这正是原文表达的意思,如果作形容词则表示“复合后的元函数”,这是“复合”的结果。这样似乎更好一些:)。
[13] 译注:这里所说的lambda表达式的各个参数并非该lambda接受的“外界”参数,举个例子:mul<plus<_1,_2>,minus<_1,_2> >这个lambda表达式的参数就是 plus<_1,_2>和minus<_1,_2>而它们各自又都是lambda表达式,所以它们会先被求值,然后将结果传给mul。
[14] 译注:事实上,lambda表达式的求值是个递归的过程。
[15] MPL缺省提供了5个占位符。MPL参考手册的Configuration Macros部分有关于如何改变提供的占位符的数目的描述。
[16] 译注:如果该占位符为_N,那么就会返回实际参数中的第N个参数,占位符的“占位”的意思就是:_N“占”的是第N个参数的位置。
[17] 译注:lazy evaluation的意思是“不到必要时不求值”。
[18] 译注:这里,“命名(naming)”的意思是,仅仅给它一个名字(意味着“仅仅实例化它的名字”),而并不对该计算求值(意味着“并不实例化该类”)。
[19] 译注:一个不错的例子是apply_if,其详细介绍见boost的官方文档。
[20] 译注:这里的原文写得相当拗口,所以译文遵循前文的定义。含义一样。