第三章 深度探索元函数 (1)

类别:编程语言 点击:0 评论:0 推荐:

          《C++ Template Metaprogramming》

第三章 深度探索元函数

              david abraham&Aleksey Gurtovoy著

               刘未鹏([email protected]) 译

 

有了前面的基础知识作铺垫,我们来考察模板元编程技术的一个最基本的应用——为传统的不进行类型检查的操作添加静态类型检查。为此,我们将考察一个有关工程科学的实例——几乎在所有涉及科学计算的代码中都可以找到它的应用。在考察该例子的过程中,你将会学到一些重要的新的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,我们会得到一个编译错误,及时防止错误在整个程序中蔓延。

 

 

[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的类模板,这两个称呼在后面将会多次提到,请读者注意它们的区别。

本文地址:http://com.8s8s.com/it/it27268.htm