第三章 深入元函数
到此所做的铺垫,我们已经准备好了探索模板元程序技术最基本的使用例子:给传统的未检查的操作添加静态类型检查。我们从科学与工程中的实际例子出发一看究竟,这个例子几乎在所有的数值代码中都有应用。一路走来,你会学到一些最重要的新概念,并对在更高级别上使用 MPL 有一个了解。
3 . 1 量纲分析
在论文中做物理计算的第一个规则就是数值的操作都不是单独的:大多数的数量都与一个量纲相关,只是我们自己大胆地忽略了它。当计算变得越来越复杂时,对于量纲的跟踪,则是使我们不至于将诸如质量赋给一个本该是长度或者加速度,由此而建立此一个数值类型系统。
手动检查类型则是乏味的,结果呢则很容易出错。当人们开始觉得无味时,他们的注意力就是偏离,即而容易出错。那么对于类型的检查不正好就是计算机最善长的工作?如果我们能够为量纲与数量之间建立起一种 C++ 类型框架,那么我们就能够在公式造成严重的问题之前就能发现问题。
防止不同量纲的数量之间进行互操作并不是件很难的事情,我们可能将不同的量纲以不同的类型来表示,因此相同的类型之间才可以共同工作。而使得问题更为有趣的是,为了产生新的更复杂的量纲,不同的量纲通过乘或除是可以混合起来使用的。例如,牛顿定律表示了力、质量与加速度之间的关系:
F = ma
因为质量与加速度有不同的量纲,而力的量纲则必须以某种方式反映他们的结合。实际上,加速度的量纲已经做了这样的组合,也就是速度相对于时间的变化量:
dv/dt
因为速度只是长度 (l) 相对于时间 (t) 的变化量,因此加速度的根本量纲是:
(l/t)/t = l/t2
而实际上,加速度一般都以“米 / 秒平方”来进行衡量,因此力的量纲则是:
ml/t2
因此,力一般也以 kg(m/s2 ) 来进行衡量,或者“千克 - 米 / 秒平方”。当进行数量与质量的数值进行相乘时,他们的量纲也进行相乘,并且与结果一起,保证了结果有意义。这样的登记工作的正式名字则是“量纲分析”,我们的下一个工作则是用 C++ 的类型系统来实现这样的规则。 John Barton 和 Lee nackman 是第一个在他们的学术书籍中介绍如何实现的这种功能的人。我们将按元程序的方式来回顾他们的实现方式。
3.1.1 量纲的表示
有一个国际标准叫做“国际单位制”将数量都表示为质量、长度、位置、时间、电荷、温度、密度以及角度这此量纲的组合。为了通用性,我们的系统应该可以表示七个或者更多的基本量纲。而它也需要表示这些量纲不同组合的能力,如力,则是通过基本的量纲这间进行乘或者除表示。
一般来说,量纲的组合就是基本量纲我幂积 [1] 。如果我们在运行时来完成这些操作,我们则可以使用 int 的数组,而数组中的每一个元素则表示不同的量纲的幂指数:
[1] 除数只是负的幂,因为 1/x=x-1 。
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, 0};
这就是 mlt¬¬¬-2 。然而,如果你想将量纲纳入类型系统,则数组方式无法做到:他们都是相同的类型。相反,我们需要类型本身就代表了数值序列,因此两个质量有相同的类型,而质量与长度则是不同的类型。
幸运的是 MPL 给了我们提供了类型序列。如,我们能像下面那样创建内建有符号类型的序列:
#include <boost/mpl/vector.hpp>
typedef boost::mpl::vector<
signed char, short, int, long> signed_types;
那么,我们如何来使用这些类型序列来表示数字呢?就像数值元函数传递并返回一个包装类型,此类型有内嵌的 ::value 成员,这样,数值序列就真正成为包装类型的序列了(这是别一种多态)。为了使这类的事情更加容易, MPL 提供了 int_<N> 这样的类型,这个类型通过内嵌的 ::value 代表了其整型参数。
#include <boost/mpl/int.hpp>
namespace mpl = boost::mpl; // namespace alias
static int const five = mpl::int_<5>::value;
名字空间别名
namespace alias = namespace-name;
上面的语法声明了 namespace-name 的一个同义语。本书中的许多的例子都使用 mpl:: 来表示 boost::mpl:: ,省略别名部分,也是合法的 C++ 语法。
事实上,库中包括了一整套整型常数的包装类型,如 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 的另一个版本。
如果我们想,我们也可以定义一些组合量纲:
// base dimension: 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 表示数量
上面所列的类型依然是一个元数据,为了在运算时进行类型检查,我们需要将他们与我们的运行时数据进行特别的绑定。一个简单的数值包装器,针对其量纲对数值类型进行特化,就可以达到这样的目标:
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 );
注意, Dimensions 除了模板参数列表以外,没有出现在 quantity 这个类型的定义的任何地方 ; 它唯一的角色使得 l 与 m 有不同的类型。因为在我们出错的情况下确实会将 length 的值赋给 mass :
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 = len2 + quantity<float,mass>( 3.7f ); // error
3.1.4 实现相乘
相乘比起加减起来要复杂一点。到目前为止,参数的量纲及结果都是相同的类型,但是对于相乘来说,结果通常都与参数的量纲是不相同的。对于相乘,关系如下:
(x)a (x)b =x(a+b)
上面的等式说明,结果量纲的指数就是参数量纲之和。相除则相似,只是相加则变成相减。
对两个序列进行相应元素进行组合,我们使用到 MPL 的 transform 算法。 transform 算法是一个元函数,该元函数通过对两个输入序列进行并行迭代,将元素的每一个元素传递给任意一个二元元函数,然后将结果放入到输出序列中。
template <class Sequence1, class Sequence2, class BinaryOperation>
struct transform; // returns a Sequence
如果你认得 STL 的接收两个输入序列的 transform 算法的话,上面的签名,则与其非常相似:
template <
class InputIterator1, class InputIterator2
, class OutputIterator, class BinaryOperation
>
void transform(
InputIterator1 start1, InputIterator2 finish1
, InputIterator2 start2
, OutputIterator result, BinaryOperation func);
我们现在只要传递一个 BinaryOperation 参数到 mpl::transform 中对量纲进行相加相减。如果你看了 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 ( … ) , BOOST_STATIC_ASSERT 可以在类域内使用,这使得我们可以将基放在元函数中。查看第八章以取得更深入的讨论。
到此,我们似乎看到了解决方法,但是我们还远未到达。在实现 operator* 操作符时对 transform 算法的天真的用法,会产生编译时错误:
#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) { ... }
编译错误是因为原型说,元函数参数必须是类型,而 plus 不是一个类型,但是是一个类模板。最终,我们需要使得像 plus 这样原元函数适合元数据模式。
一种自然在元函数与元数据之间引入一层多态的方法就是配置包装器用法,使其给我们一种即是整数常数也是类型的多态。不用内嵌整型常数,我们只需要在类模板中嵌入一个元函数类:
struct plus_f
{
template <class T1, class T2>
struct apply
{
typedef typename mpl::plus<T1,T2>::type type;
};
};
定义:
一个元函数类就是一个类带有一个公共的可访问的叫做 apply 的内嵌元函数类。
尽管元函数是一个类模板而不是一个类型,而在一个非模板类内嵌一个元函数类的包装类则一个类型。因为元函数操作类型而又返回类型,一个元函数类也可以将当成参数被传递给另一个元函数,或者当成结果被返回。
最终,我们有一个 BinaryOperation 类型,因此可以传递给 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 千克 的手提电脑的重力,就是重要加速度( 9.8m /sec2 )乘上手提电脑的质量:
quantity<float,mass> m(5.0f);
quantity<float,acceleration> a(9.8f);
std::cout << "force = " << (m * a).value();
我们的 operator* 乘上运行时值(结果是 6.0f ),我们的元程序代码使用 transform 来对基础量纲的指数的元序列进行相加,因此结果类型包括了一组新的指数序列,类似如下:
vector_c<int,1,1,-2,0,0,0,0>
但是,当我们尝试如下时:
quantity<float,force> f = m * a;
这将出现一个问题,尽管 m*a 的结果确实表示了质量、长度及时间相应乘 1 、 1 、- 2 ,也就是力的指数。但是 transform 返回在类型将不是 vector_c 的一个特化。实际上, transform 通常针对输入参数的元素创建一个带有特定元素的新的序列:带有与 vector_c<int,1,1,-2,0,0,0,0> 相同的序列属性的类型,但是却是一个不同的类型。如果你想看到类型的全名,你可以自己编译一下这个例子,然后看一下错误信息,但是确切的细节并不重要。实际上是力代表了一个不同的类型,因此上面的赋值会失败。
为了解决这个问题,我们可以添加隐式转换,将相乘的结果转换成 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())
{
}
...
不幸的是,这样的通过转换破坏了我们整个目的,因为它可以进行下面这样的转换:
// 会产生力,而不是质量
quantity<float,mass> bogus = m * a;
我们可以使用 MPL 的算法来解决这个问题,那就是检测两个序列有相同的元素:
template <class OtherDimensions>
quantity(quantity<T,OtherDimensions> const& rhs)
: m_value(rhs.value())
{
BOOST_STATIC_ASSERT((
mpl::equal<Dimensions,OtherDimensions>::type::value
));
}
现在,如果两个数量的量纲不能匹配,则断言就会造成一个编译时错误。
3.1.5 实现除法
除法与乘法有相似之处,但不是使用指数相加,而是相减。为了避免重复写 plus_f ,我们可以用下面的技巧,使得 minus_f 更加简单:
struct minus_f
{
template <class T1, class T2>
struct apply
: mpl::minus<T1,T2> {};
};
在这里 minus_f::apply 使用继承来暴露基类 mpl:minus 的内嵌类型,因此我不需要写:
typedef typename ...::type type
在这我们不用写 typename (实际是非法的),因为编译器知道在 apply 的初始化列表中依赖类型一定是基类 [2] 。这个强大的简单技术叫做元函数前推。在后面的章节中,这样的技术会常常使用。 [5]
[2] 你可能正纳闷,相同的方式也可以应用到 plus_f ,但是因为它有一些微妙,因此我们先介绍一个更为啰嗦的但直接的方式。
[5] 使用 EDG 类的编译器的用户可能需要参数 Appendix C 来了解元函数前推的说明。你可以通过处理 __EDG_VERSION__ 这个宏来判断你的编译器是否是 EDG 类的编译器。因为这个宏由 EDG 类的编译器定义。
尽管在语法上有些技巧,但是写这样没有琐碎的类型来包装已有的元函数不久就会变得相当的乏味。就算 minux_f 的定义要比 plus_f 的字符要少得多,但是依然还是很多。幸运的是, MPL 给了我们一个更简单的方法来传递元函数。
用下面的方式来调用 transform ,我们可以不需要写整个元函数类:
typename mpl::transform<D1,D2, mpl::minus<_1,_2> >::type
那些看起来奇怪的参数 ( _1 和 _2 )叫做占位符,当 BinaryOperation 被调用时他们才会被真正符号化。由 _1 、 _2 所表示的第一个和第二个参数将被传递给 minus ,整个类型 mpl::minus<_1,_2> 就是所谓的占位符表达式。
注意:
MPL 点位符位于 mpl::placeholder 名字空间中,并且定义在 /mpl/placeholder.hpp 文件中。在本书中,我们都假设你都会这样写:
#include<boost/mpl/placeholders.hpp>
using namespace mpl::placeholders;
因此,你可以在不用限定符的情况下访问这些占位符。
这是我们用点位符表达式写的除法操作符:
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() );
}
这个代码就更简单了,我们甚至可以再进一步简化。通过重构计算新的量纲到我们的元函数:
template <class D1, class D2>
struct divide_dimensions
: mpl::transform<D1,D2,mpl::minus<_1,_2> > // forwarding again
{};
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 的数,这些都是乏味的计算,但是如果你没有处理对,它们就是会毁掉整个程序的那类问题,如果我们写 a/f 而不是 f/a ,将会有一个编译时错误,这将会阻止将错误在我们的程序中传递。