《C++ Template Metaprogramming》第三章——深度探索元函数

news/2024/7/7 7:37:09

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)

模板元编程是最高阶最抽象也是最强大的能力,也是最前卫的技术,其带来的复用性以及代码强大的表达能力令人瞠目结舌,本章深入讨论元编程的机理以及应用,展现中最高阶的语义有了前面的基础知识作铺垫,我们来考察模板元编程技术的一个最基本的应用[1]单位[2]分析物理计算的首要原则是:数值并非是独立的单位。而我们一不小心就会将单位置之脑后,这是件很危险的事情。随着计算变得越来越复杂,维持物理量的正确单位能够避免诸如手动检查类型是件单调而乏味的工作,并且容易导致错误。当人们感到厌烦时,注意力就会分散,从而容易犯错误。然而,类型检查不正是计算机擅长的工作吗?如果我们能够为物理量和单位构建一个阻止单位不同的物理量互操作并不难F=ma

由于质量和加速度有着不同的单位,所以力的单位必须是两者的结合。事实上,加速度的单位就已经是个dv/dt

又因为速度即(l/t)/t=l/t2

并且,加速度通常以ml/t2

也就说,力通常以kg(m/s2)单位分析

,而我们的下一个任务就是在C++类型系统中实现它。 单位的表示国际标准单位制规定了物理量的标准单位为:质量(力(kg(m/s2))的单位这种经过几个基本单位乘除而成的复合单位。一般来说,一个复合单位可以看成若干基本单位的幂的乘积
[3]。如果要表示这些幂次以便在运行期可以操纵它们,我们可以使用一个数组,其七个元素每个对应一个不同的单位,而其值表示对应单位的幂次:

    ={1,0,0,0,0,0,0};

  ={0,1,0,0,0,0,0};

    ={0,0,1,0,0,0,0};

根据这种表示法,力的表示如下:

也就是说,[4]中去,这些数组就无法胜任了:它们的类型全都相同,都是自身能够表示数值序列的类型,这样质量和长度的类型就是不同的,而两个质量的类型则是相同的。幸运的是,类型序列的设施。例如,我们可以构建一个有符号整型的序列:

那么,我们如何用类型序列来表示单位[5]呢?由于数值型的元函数传递和返回的类型是具有内嵌

[6]

 

事实上,MPL库包含了一整套整型常量的外覆类,如long_bool_等,每个外覆类对应一个不同类型的整型常量。

 

现在,我们可以将基本单位构建如下:

 

 , mpl::int_<0>, mpl::int_<0>, mpl::int_<0>

mass

;

 , mpl::int_<0>, mpl::int_<0>, mpl::int_<0>

length

;

 

整型序列外覆类,它允许我们写出类似下面的代码:

你可以将这个特殊的如果我们愿意,我们还可以定义一些复合单位:

并且,有时候,标量的单位(如

物理量的表示上面所列的类型仍然是纯粹的元数据。要想对真实的计算进行类型检查,我们还需要以某种方式将它们(元数据)绑定到运行时数据。一个简单的数值外覆类

 

现在,我们有了将数值和单位联系到一起的办法。例如,我们可以说:

 

1.0f );

2.0f );

注意到在唯一作用是确保不可能错误地将长度赋给质量:

实现加法和减法因为参数的类型(单位)必须总是匹配,所以我们现在可以轻易的写出加法和减法的规则:

  return quantity (x.value() + y.value());

  return quantity (x.value() - y.value());

这样,我们就可以写出类似下面的代码:

1.0f );

2.0f );

并且,我们不能将不同单位的量相加:

mass

>( 3.7f ); // error

实现乘法乘法比加减法复杂一些。到目前为止,运算的参数和结果的单位都是一样的,但是做乘法时,结果的单位往往和两个参数的单位都不相同。对于乘法,下面的式子:

意味着结果的单位的指数为相应参数的单位的指数和。商与此类似,为指数差。为此,我们使用

如果你熟悉运行期的输入序列:

 , InputIterator2 start2

 , OutputIterator result, BinaryOperation func);

现在我们只需要向

到目前为止,看起来我们已经有了一个解决方案,像这样:

但是很抱歉,这还不够!现在如果你试图使用这个从某种意义上说,这就要求在元函数和元数据之间引入多态,一个很自然的途径是使用外覆类惯用手法元函数类

[7]

中:

定义:元函数类是指内嵌有名为虽然元函数是模板而非类型,但是元函数类却以一个普通的非模板类将其包覆起来,使其成为一个类型。因为元函数操作和返回的都是类型,所以元函数类也可以被作为参数传递给另一个元函数,而元函数也可以返回一个元函数类。从而,我们得到了一个

 , typename mpl::transform ::type //new dimensions

现在,如果我们计算一个

5.0f

);

9.8f );

我们自定义的

然而,如果我们试图写:

我们会遇到一点问题。尽管为了解决这个问题,我们可以添加一个从乘法的结果类型到

然而,很不幸的是,这样一个通用的转换彻底违背了我们原来的意图,一旦有了这个转换,我们就可以写出下面的代码:

这简直糟透了!幸运的是,我们可以通过另一个

  : m_value(rhs.value())

     mpl::equal ::type::value

  ));

现在,如果两个单位不匹配,那么这个 实现除法除法和乘法类似,乘法将指数相加,而除法将指数相减。显然,作除法的元函数类

  struct apply

这里,

这个强有力的简化代码的手法被称为元函数转发。后面我们还会频繁使用它。注意,我们不用在尽管有这样的语法技巧来简化代码,但一遍遍地写这些简单之极的外覆类仍然会很快让人感到厌烦。虽然

minus

< _1, _2> >::type

其中有两个看起来很奇怪的参数(占位符,这里它们的意思是:当占位符表达式

附注这样,像使用占位符后的

 , typename mpl::transform <_1,_2> >::type

代码明显变得更为简洁了(因为用不着额外定义一个

divide_dimensions

divide_dimensions ::type>

divide_dimensions ::type>(

现在我们可以验证膝上型计算机的重力是否计算正确,通过一个逆向的计算,我们得到其质量,然后将它与条件给出的计算机质量比较:

如果一切正常,那么高阶元函数(在前面一节,我们传递或返回元函数时使用了两种格式高阶函数式编程(。操纵其它函数的函数被称为高阶函数。所以,现在我们已经见识过了高阶元函数的强大能力,下面我们将尝试创建新的高阶元函数。为了探究其底层机理,让我们先来看一个简单的例子。我们的任务是写一个名为这个例子看起来没什么价值如果

  typedef typename F::template apply ::type once; //f(x)

  typedef typename F::template apply ::type type;//f(f(x))

或者使用元函数转发:

     typename F::template apply ::type

语言附注依赖名字(并且该名字指的是一个成员模板时,我们必须使用依赖

显然,在每次使用元函数类的时候都在

  : UnaryMetaFunctionClass::template apply

现在,

我们来看一下

  struct apply : boost::add_pointer {};

  boost::is_same<

    twice ::type

我们可以看出,将处理占位符虽然我们的

但是我们只要考察一下

我们并没有得到想要的行为。下面该怎么办呢?想想看,既然 元函数我们可以使用生成

一个元函数类:

  boost::is_same<

后面我们将把表达式

。这个称呼的含义是计算[8]

)的计算理论中的一部分。之所以使用尽管

       typename mpl::lambda ::type

      , X

现在我们可以将

          p = &x;

元函数调用

你可以将[9][10],并用它们来调用元函数类。例如:

原则如果你要在你的元函数中调用其某个参数(即:将某个参数作为元函数类来调用的其它能力 部分函数应用(考虑二元的元函数被用来创建了一个一元

将一集实参绑定到某个函数的形参的一个子集的过程在函数式编程语言[11]中被称为部分函数应用 复合元函数[12]

可以看出,它是三个元函数(复合

体。
当对一个[13],如果是,则先将它们求值,并将这些(本身为[14]的细节现在你对 占位符

定义占位符是一个形为 实现([15]。占位符的实现像这样:

前面说过,调用元函数类就是调用其内嵌的[16]。再然后求值(返回)的结果会替换 匿名()占位符匿名占位符是个非常特殊的占位符,其定义如下:

其实现细节并不重要。对于匿名占位符,你所需知道的就是:它是被特殊对待的。当一个在某个给定的模板特化体中的第例如,下面的表                             

     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>

     >

占位符表达式的定义现在你应该已经知道了占位符的含义了。既然如此,我们可以定义占位符表达式如下:

定义一个占位符表达式是:一个占位符或者一个其参数至少有一个为占位符表达式的模板特化体。换句话说,一个占位符表达式始终包含(至少)一个占位符。 和非元函数()模板关于占位符表达式,一个尚未讨论的细节是:为了使普通模板更容易融入元编程,例如,

但是现在由于有了这个特殊规则,我们可以简单地写:

懒惰的重要性回顾上一章提到的

无参(

  struct apply : boost::add_pointer {};

注意到元函数接受了它的参数后仍可以被延迟调用。当一个元函数只是被选择性的使用时,我们可以使用惰性求值[17]

来减少编译时间。有时,通过命名[18]一个无效的计算而并不去实际执行它,我们还可以避免扭曲程序结构[19]。我们对细节到目前为止,你对一般的模板元编程和元函数转发(使用[20]元函数类(将编译期函数形式化的最基本方法,由此,编译期函数可以被看作多态的元数据,也就是看作一个类型。元函数类是个内嵌有名为本书中的大部分例子都用到了component-name.hpp>

然而,如果高阶函数(操作或返回函数的函数。利用其它元数据使元函数成为多态的是高阶元编程中的一个关键之处。表达式简单的说,元函数类和占位符表达式占位符表达式部分函数应用和复合元函数的目的。正如你将会在本书中随处可见的,这些特性给予我们惊人的能力,允许我们从原始的元函数构造出几乎任意复杂的类型计算

         x is convertible to 'int'

      && x is not 'char'

      && x is not a floating type

    boost::is_convertible<_1,int>

   , mpl::not_ <_1,char> >

   , mpl::not_ <_1> >

  >

占位符表达式使我们不必(为元函数)写新的(外覆)元函数类,实现了算法复用的目的。而这种能力在元函数(元函数(一个元函数,其行为是:以其余的参数去调用其第一个参数,后者必须是个惰性求值(一种将计算推迟到其结果被要求的时候的策略。这种策略可以避免所有不必要的计算和不必要的错误。元函数仅仅在我们访问其内嵌的


[1] 译注:MPLBoost库里面的一个子库。用于支持模板元编程。下文会多次提到这个MPL库。

[2] 译注:这里原文为Dimensional Analysis,这里的Dimensional并非作通常意义上的维度解释。而是作为物理上的单位解释,因为下文讲的正是如何在编译期对物理量进行单位检查,进而实现一个编译期的健全的单位系统。

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

[8] http://en.wikipedia.org/wiki/Lambda_calculus

[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 <_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] 译注:这里的原文写得相当拗口,所以译文遵循前文的定义。含义一样。





http://www.niftyadmin.cn/n/3654913.html

相关文章

动画:《大前端吊打面试官系列》 之原生 JavaScript 精华篇

&#x1f4da;更新日志 文中所有修改或补充内容&#xff0c;会在日志中实时更新。 2020/01/7 开始决定写近十几万字前端面试系列&#xff0c;规划整个系列目录提纲。2020/01/8 写完部分“面试官到底考察你什么”内容。2020/01/9 继续完善”面试官到底考察你什么“内容。2020/01…

《C++ Template Metaprogramming》译序——经典之后的经典

——写在C图书出版史上又一部经典著作问世之前刘未鹏 /文C图书界沉寂久矣&#xff01;至少我是这么认为的。国内引进第一批C经典书籍的时候我是亲历的。感觉真是“忽如一夜春风来&#xff0c;千树万树梨花开”。别误会&#xff0c;我说的这个第一批并非《C Programming Languag…

旧话重提:pImpl惯用手法的背后

旧话重提&#xff1a;pImpl惯用手法的背后刘未鹏pImpl惯用手法已经太老了&#xff0c;老得人们已经记不得它是什么时候被提出的了。像这么一个老得牙都掉了的东东几乎是肯定讲不出什么新意出来的。本文也不例外&#xff0c;只不过&#xff0c;这里我们并不想提出什么新的创意&a…

《C++ Template Metaprogramming》附录A——预处理元编程

《C Template Metaprogramming》附录A&#xff1a;预处理元编程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中最高阶最抽象&am…

为什么C++编译器不能支持对模板的分离式编译

为什么C编译器不能支持对模板的分离式编译刘未鹏(pongba)C的罗浮宫(http://blog.csdn.net/pongba)首先&#xff0c;一个编译单元&#xff08;translation unit&#xff09;是指一个.cpp文件以及它所#include的所有.h文件&#xff0c;.h文件里的代码将会被扩展到包含它的.cpp文件…

在C++中侦测内嵌型别的存在(rev#2)

在中侦测内嵌类型的存在http://blog.csdn.net/pongba)动机假设一所大学的注册系统提供了一个注册函数&#xff1a;class T>typename T::person_tag());而对于注册者有以下几种标识&#xff1a;还有class T> void Register(T p, student_tag){...} // 注册学生class T>…

boost源码剖析之:泛型指针类any之海纳百川(rev#2)

boost源码剖析之&#xff1a;泛型指针类any之海纳百川(rev#2)刘未鹏C的罗浮宫(http://blog.csdn.net/pongba)动机C是强类型语言&#xff0c;所有强类型语言对类型的要求都是苛刻的&#xff0c;类型一有不合编译器就会抱怨说不能将某某类型转换为某某类型&#xff0c;当然如果在…

Android学习第一天————布局方式(线性布局、表格布局、图层布局)

一、线性布局&#xff08;LinearLayout&#xff09; 线性布局不会换行&#xff0c;当组件一个一个排列到头时&#xff0c;剩下的组件不会被显示 XML文件控制&#xff08;一些重要属性&#xff09; android:orientation"vertical/horizontal"垂直/水平 android:grav…