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

2024-01-18 04:38

本文主要是介绍《C++ Template Metaprogramming》第三章——深度探索元函数,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

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




这篇关于《C++ Template Metaprogramming》第三章——深度探索元函数的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/618097

相关文章

C++中实现调试日志输出

《C++中实现调试日志输出》在C++编程中,调试日志对于定位问题和优化代码至关重要,本文将介绍几种常用的调试日志输出方法,并教你如何在日志中添加时间戳,希望对大家有所帮助... 目录1. 使用 #ifdef _DEBUG 宏2. 加入时间戳:精确到毫秒3.Windows 和 MFC 中的调试日志方法MFC

五大特性引领创新! 深度操作系统 deepin 25 Preview预览版发布

《五大特性引领创新!深度操作系统deepin25Preview预览版发布》今日,深度操作系统正式推出deepin25Preview版本,该版本集成了五大核心特性:磐石系统、全新DDE、Tr... 深度操作系统今日发布了 deepin 25 Preview,新版本囊括五大特性:磐石系统、全新 DDE、Tree

Oracle的to_date()函数详解

《Oracle的to_date()函数详解》Oracle的to_date()函数用于日期格式转换,需要注意Oracle中不区分大小写的MM和mm格式代码,应使用mi代替分钟,此外,Oracle还支持毫... 目录oracle的to_date()函数一.在使用Oracle的to_date函数来做日期转换二.日

深入理解C++ 空类大小

《深入理解C++空类大小》本文主要介绍了C++空类大小,规定空类大小为1字节,主要是为了保证对象的唯一性和可区分性,满足数组元素地址连续的要求,下面就来了解一下... 目录1. 保证对象的唯一性和可区分性2. 满足数组元素地址连续的要求3. 与C++的对象模型和内存管理机制相适配查看类对象内存在C++中,规

Node.js 中 http 模块的深度剖析与实战应用小结

《Node.js中http模块的深度剖析与实战应用小结》本文详细介绍了Node.js中的http模块,从创建HTTP服务器、处理请求与响应,到获取请求参数,每个环节都通过代码示例进行解析,旨在帮... 目录Node.js 中 http 模块的深度剖析与实战应用一、引言二、创建 HTTP 服务器:基石搭建(一

在 VSCode 中配置 C++ 开发环境的详细教程

《在VSCode中配置C++开发环境的详细教程》本文详细介绍了如何在VisualStudioCode(VSCode)中配置C++开发环境,包括安装必要的工具、配置编译器、设置调试环境等步骤,通... 目录如何在 VSCode 中配置 C++ 开发环境:详细教程1. 什么是 VSCode?2. 安装 VSCo

C++11的函数包装器std::function使用示例

《C++11的函数包装器std::function使用示例》C++11引入的std::function是最常用的函数包装器,它可以存储任何可调用对象并提供统一的调用接口,以下是关于函数包装器的详细讲解... 目录一、std::function 的基本用法1. 基本语法二、如何使用 std::function

【C++ Primer Plus习题】13.4

大家好,这里是国中之林! ❥前些天发现了一个巨牛的人工智能学习网站,通俗易懂,风趣幽默,忍不住分享一下给大家。点击跳转到网站。有兴趣的可以点点进去看看← 问题: 解答: main.cpp #include <iostream>#include "port.h"int main() {Port p1;Port p2("Abc", "Bcc", 30);std::cout <<

深入探索协同过滤:从原理到推荐模块案例

文章目录 前言一、协同过滤1. 基于用户的协同过滤(UserCF)2. 基于物品的协同过滤(ItemCF)3. 相似度计算方法 二、相似度计算方法1. 欧氏距离2. 皮尔逊相关系数3. 杰卡德相似系数4. 余弦相似度 三、推荐模块案例1.基于文章的协同过滤推荐功能2.基于用户的协同过滤推荐功能 前言     在信息过载的时代,推荐系统成为连接用户与内容的桥梁。本文聚焦于

C++包装器

包装器 在 C++ 中,“包装器”通常指的是一种设计模式或编程技巧,用于封装其他代码或对象,使其更易于使用、管理或扩展。包装器的概念在编程中非常普遍,可以用于函数、类、库等多个方面。下面是几个常见的 “包装器” 类型: 1. 函数包装器 函数包装器用于封装一个或多个函数,使其接口更统一或更便于调用。例如,std::function 是一个通用的函数包装器,它可以存储任意可调用对象(函数、函数