(转)现代C++函数式编程AHUWangXiao

导读:本文作者从介绍函数式编程的概念入手,分析了函数式编程的表现形式和特性,最终通过现代C++的新特性以及一些模板云技巧实现了一个非常灵活的pipeline,展示了现代C++实现函数式编程的方法和技巧,同时也体现了现代C++的强大威力和无限可能。

函数式编程是一种编程范式,它有下面的一些特征:

C++98/03中的函数对象,和C++11中的Lambda表达式、std::function和std::bind让C++的函数式编程变得容易。我们可以利用C++11/14里的新特性来实现高阶函数、链式调用、惰性求值和柯理化等函数式编程特性。本文将通过一些典型示例来讲解如何使用现代C++来实现函数式编程。

高阶函数就是参数为函数或返回值为函数的函数,经典的高阶函数就是map、filter、fold和compose函数,比如Scala中高阶函数:

上面的例子中,有的是参数为函数,有的是参数和返回值都是函数。高阶函数不仅语义上更加抽象泛化,还能实现“函数是一等公民”,将函数像data一样传来传去或者组合,非常灵活。其中,compose还可以实现惰性求值,compose的返回结果是一个函数,我们可以保存起来,在后面需要的时候调用。

pipeline把一组函数放到一个数组或是列表中,然后把数据传给这个列表。数据就像一个链条一样顺序地被各个函数所操作,最终得到我们想要的结果。它的设计哲学就是让每个功能就做一件事,并把这件事做到极致,软件或程序的拼装会变得更为简单和直观。Scala中的链式调用是这样的:

s(x)=(1tox)|>filter(x=>x%2==0)|>map(x=>x*2)用法和UnixShell的管道操作比较像,|前面的数据或函数作为|后面函数的输入,顺序执行直到最后一个函数。

这种管道方式的函数调用让逻辑看起来更加清晰明了,也非常灵活,允许你将多个高阶函数自由组合成一个链条,同时还可以保存起来实现惰性求值。现代C++实现这种pipeline也是比较容易的,下面来讲解如何充分借助C++11/14的新特性来实现这些高阶函数和pipeline。

根据前面介绍的pipeline表现形式,可以把pipeline分解为几部分:高阶函数,惰性求值,运算符|、柯里化和pipeline,把这几部分实现之后就可以组成一个完整的pipeline了。下面来分别介绍它们的实现技术。

函数式编程的核心就是函数,它是一等公民,最灵活的函数就是高阶函数,现代C++的算法中已经有很多高阶函数了,比如for_each,transform:

std::vectorvec{1,2,3,4,5,6,7,8,9}//接受一个打印的Lambda表达式std::for_each(vec.begin(),vec.end(),[](autoi){std::cout<

classuniversal_functor{public:templateautooperator()(Args&&...args)const->decltype(globle_func(std::forward(args)...)){returngloble_func(std::forward(args)...);}};上面的函数对象内部包装了一个普通函数的调用,当函数调用的时候实际上会调用普通函数globle_func,但是这个代码不通用,它无法用于其他的函数。为了让这个转换变得通用,我们可以借助一个宏来实现function到functor的转换。

#definedefine_functor_type(func_name)classtfn_##func_name{\public:templateautooperator()(Args&&...args)const->decltype(func_name(std::forward(args)...))\{returnfunc_name(std::forward(args)...);}}//testcodeintadd(inta,intb){returna+b;}intadd_one(inta){return1+a;}define_functor_type(add);define_functor_type(add_one);intmain(){tnf_addadd_functor;add_functor(1,2);//resultis3tfn_add_oneadd_one_functor;add_one_functor(1);//resultis2return0;}我们先定义了一个宏,这个宏根据参数来生成一个可变参数的函数对象,这个函数对象的类型名为tfn_加普通函数的函数名,之所以要加一个前缀tfn_,是为了避免类型名和函数名重名。define_functor_type宏只是定义了一个函数对象的类型,用起来略感不便,还可以再简化一下,让使用更方便。我们可以再定义一个宏来生成转换后的函数对象:

#definemake_globle_functor(NAME,F)constautoNAME=define_functor_type(F);//testcodemake_globle_functor(fn_add,add);make_globle_functor(fn_add_one,add_one);intmain(){fn_add(1,2);fn_add_one(1);return0;}make_globle_functor生成了一个可以直接使用的全局函数对象,使用起来更方便了。用这个方法就可以将普通函数转成pipeline中的函数对象了。接下来我们来探讨实现惰性求值的关键技术。

惰性求值是将求值运算延迟到需要值时候进行,通常的做法是将函数或函数的参数保存起来,在需要的时候才调用函数或者将保存的参数传入函数实现调用。现代C++里已经提供可以保存起来的函数对象和lambda表达式,因此需要解决的问题是如何将参数保存起来,然后在需要的时候传给函数实现调用。我们可以借助std::tuple、type_traits和可变模版参数来实现目标。

templateinlineautotuple_apply_impl(constF&f,conststd::index_sequence&,conststd::tuple&tp){returnf(std::get(tp)...);}templateinlineautotuple_apply(constF&f,conststd::tuple&tp)->decltype(f(std::declval()...)){returntuple_apply_impl(f,std::make_index_sequence{},tp);}intmain(){//testcodeautof=[](intx,inty,intz){returnx+y-z;};//将函数调用需要的参数保存到tuple中autoparams=make_tuple(1,2,3);//将保存的参数传给函数f,实现函数调用autoresult=tuple_apply(f,params);//resultis0return0;}上面的测试代码中,我们先把参数保存到一个tuple中,然后在需要的时候将参数和函数f传入tuple_apply,最终实现了f函数的调用。tuple_apply实现了一个“魔法”将tuple变成了函数的参数,来看看这个“魔法”具体是怎么实现的。

tuple_apply_impl实现的关键是在于可变模版参数的展开,可变模版参数的展开又借助了std::index_sequence

pipeline的一个主要表现形式是通过运算符|来将data和函数分隔开或者将函数和函数组成一个链条,比如像下面的unixshell命令:

psauwwx|awk'{print$2}'|sort-n|xargsechoC++实现类似的调用可以通过重载运算符来实现,下面是data和函数通过|连接的实现代码:

templateautooperator|(T&¶m,constF&f)->decltype(f(std::forward(param))){returnf(std::forward(param));}//testcodeautoadd_one=[](autoa){return1+a;};autoresult=2|add_one;//resultis3除了data和函数通过|连接之外,还需要实现函数和函数通过|连接,我们通过可变参数来实现:

templateinlineautooperator|(fn_chain&&chain,F&&f){returnchain.add(std::forward(f));}//testcodeautochain=fn_chain<>()|(filter>>[](autoi){returni%2==0;})|ucount|uprint;其中fn_chain是一个可以接受任意个函数的函数对象,它的实现将在后面介绍。通过|运算符重载我们可以实现类似于unixshell的pipeline表现形式。

函数式编程中比较灵活的一个地方就是柯里化(currying),柯里化是把多个参数的函数变换成单参数的函数,并返回一个新函数,这个新函数处理剩下的参数。以Scala的柯里化为例:

defadd(x:Int,y:Int)=x+yadd(1,2)//3add(7,3)//10defadd(x:Int)=(y:Int)=>x+yadd(1)(2)//3add(7)(3)//10currying之后add(1)(2)等价于add(1,2),这种currying默认是从左到右的,如果希望从右到左呢,然而大部分编程语言没有实现更灵活的curring。C++11里面的std::bind可以实现currying,但要实现向左或向右灵活的currying比较困难,可以借助tuple和前面介绍的tuple_apply来实现一个更灵活的currying函数对象。

//curryingfromlefttorighttemplateautooperator<<(constUF&f,Arg&&arg)->decltype(f.templatecurry_before(std::forward(arg))){returnf.templatecurry_before(std::forward(arg));}//curryingfromrighttolefttemplateautooperator>>(constUF&f,Arg&&arg)->decltype(f.templatecurry_after(std::forward(arg))){returnf.templatecurry_after(std::forward(arg));}有了这两个重载运算符,测试代码可以写得更简洁了。

voidtest_currying(){autof=[](intx,inty,intz){returnx+y-z;};autofn=fn_to_curry_functor(f);autoresult=(fn<<1)(2,3);//0result=(fn<<1<<2)(3);//0result=(fn<<1<<2<<3)();//0result=(fn<<1>>2<<3)();//2result=(fn>>1>>2<<3)();//1}curry_functor利用了tuple的特性,内部有两个空的tuple,一个用来保存leftcurrying的参数,一个用来保存rightcurrying的参数,不断地currying时,通过tuple_cat把新currying的参数保存到tuple中,最后调用的时候将tuple成员和参数组成一个最终的tuple,然后通过tuple_apply实现调用。有了前面这些基础设施之后我们实现pipeline也是水到渠成。

通过运算符|重载,我们可以实现一个简单的pipeline:

templateautooperator|(T&¶m,constF&f)->decltype(f(std::forward(param))){returnf(std::forward(param));}//testcodevoidtest_pipe(){autof1=[](intx){returnx+3;};autof2=[](intx){returnx*2;};autof3=[](intx){return(double)x/2.0;};autof4=[](doublex){std::stringstreamss;ss<

fn_chain的实现思路是这样的:内部有一个std::tuple

templateautocall_impl(Arg&&arg,conststd::index_sequence&)const->decltype(std::get(functions_)(std::forward(arg))){returnstd::get(functions_)(std::forward(arg));}templateautocall_impl(Arg&&arg,conststd::index_sequence&)const->decltype(call_impl(std::get(functions_)(std::forward(arg)),std::index_sequence{})){returncall_impl(std::get(functions_)(std::forward(arg)),std::index_sequence{});}在调用call_impl的过程中,将std::index_sequence不断展开,先从tuple中获取第I个function,然后调用获得第I个function的执行结果,将这个执行结果作为下次调用的参数,不断地递归调用,直到最后一个函数完成调用为止,返回最终的链式调用的结果。

至此我们实现具备惰性求值、高阶函数和currying特性的完整的pipeline,有了这个pipeline,我们可以实现经典的流式计算和AOP,接下来我们来看看如何利用pipeline来实现流式的mapreduce和灵活的AOP。

前面的pipeline已经可以实现链式调用了,要实现pipeline形式的mapreduce关键就是实现map、filter和reduce等高阶函数。下面是它们的具体实现:

//MAPtemplateclassC,typenameF>autofn_map(constC&container,constF&f)->C()))>{usingresultType=decltype(f(std::declval()));Cresult;for(constauto&item:container)result.push_back(f(item));returnresult;}//REDUCE(FOLD)templateclassC,typenameF>TResultfn_reduce(constC&container,constTResult&startValue,constF&f){TResultresult=startValue;for(constauto&item:container)result=f(result,item);returnresult;}//FILTERtemplateclassC,typenameF>Cfn_filter(constC&container,constF&f){Cresult;for(constauto&item:container)if(f(item))result.push_back(item);returnresult;}这些高阶函数还需要转换成支持currying的functor,前面我们已经定义了一个普通的函数对象转换为柯里化的函数对象的方法:

templateautofn_to_curry_functor(F&&f){returncurry_functor(std::forward(f));}通过下面这个宏让curryingfunctor用起来更简洁:

#definemake_globle_curry_functor(NAME,F)define_functor_type(F);constautoNAME=fn_to_curry_functor(tfn_##F());make_globle_curry_functor(map,fn_map);make_globle_curry_functor(reduce,fn_reduce);make_globle_curry_functor(filter,fn_filter);我们定义了map、reduce和filter支持柯里化的三个全局函数对象,接下来我们就可以把它们组成一个pipeline了。

voidtest_pipe(){//testmapreducevectorslist={"one","two","three"};slist|(map>>[](autos){returns.size();})|(reduce>>0>>[](autoa,autob){returna+b;})|[](autoa){cout<>[](autos){returns.size();})|(reduce>>0>>[](autoa,autob){returna+b;})|([](inta){std::cout<

有了这个pipeline,实现灵活的AOP也是很容易的:

structperson{personget_person_by_id(intid){this->id=id;return*this;}intid;std::stringname;};voidtest_aop(){constperson&p={20,"tom"};autofunc=std::bind(&person::get_person_by_id,&p,std::placeholders::_1);autoaspect=tfn_chain|([](intid){cout<<"before";returnid+1;})|func|([](constperson&p){cout<<"after"<

我有很多年没搞c++了,所以也记不得那么多了!

函数式编程可不是这个。而是用元编程模拟ifwhilefor等语句

THE END
1.c++(编程指北篇)C++ Google Style Guide 2、头文件相关 2-1、声明与实现的分离 头文件应该只包含声明,禁止代码实现(内联函数除外)。项目可以约定自己最大内联函数长度。 头文件中应只声明对外提供的接口,不建议在头文件中声明仅用于内部实现的接口。 对于仅用于实现的头文件,建议仅将其include在实现文件中而不是头文件中。 https://zhuanlan.zhihu.com/p/514398744?utm_id=0
2.C++教程w3cschool 编程狮,随时随地学编程 下载APP | 登录 首页 入门教程 编程课程 特色实战 畅学全站 浏览:3729513 收藏:19588 C++ 教程 开始阅读 下载APP,阅读更畅快 教程说明: 了解C++: C++是C语言的继承,它是一种使用非常广泛的计算机编程语言,C++作为一种静态数据类型检查的、支持多范型的通用程序设计语言,能够支持https://m.w3cschool.cn/cpp/
3.C++教程菜鸟教程C++ 教程C++ 是一种高级语言,它是由 Bjarne Stroustrup 于 1979 年在贝尔实验室开始设计开发的。C++ 进一步扩充和完善了 C 语言,是一种面向对象的程序设计语言。C++ 可运行于多种平台上,如 Windows、MAC 操作系统以及 UNIX 的各种版本。 本教程通过通俗易懂的语言来讲解 C++ 编程语言。 现在开始学习 C++ 编程!https://m.runoob.com/cplusplus/cpp-tutorial.html
4.C++编程学习的经典网站,强烈推荐!c++11网站经典C++编程学习的经典网站,强烈推荐! C/C++是最主要的编程语言。这里列出了50名优秀网站和网页清单,这些网站提供c/c++源代码。这份清单提供了源代码的链接以及它们的小说明。我已尽力包括最佳的C/C++源代码的网站。这不是一个完整的清单,您有建议可以联系我,我将欢迎您的建议,以进一步加强这方面的清单。https://blog.csdn.net/Vincentlmeng/article/details/61420754
5.C++语言程序设计3. 练习:Python 简单编程 (作业递交链接, due 09.13) 第2 周09.14 (09.16) 课件:第二讲:C++ 编程基础 参考资料:C++ 中的关键字,C++ 运算优先级 代码:ex02.zip 1. 编程作业选讲 2. 完成第二讲的作业(练习 2.1, 2.2, 2.3) (作业递交链接, due 09.20) https://math.ecnu.edu.cn/~jypan/Teaching/Cpp/index.html
6.现代C++编程实战针对以上这些问题,吴咏炜将结合自己过去 20 多年累积的工作经验,节选现代 C++ 的重要新特性,带你进入编程实战,手把手带你重新理解 C++。相信通过本专栏的学习,你能把 C++ 当作一种实用的语言,能用它写出抽象但自然、可维护的高性能代码,并用它去更好地解决项目中的实际问题。 https://time.geekbang.org/column/intro/256
7.C++初学者编程软件推荐然而,对于初涉C++编程的学习者来说,如何选择合适的工具和资源,往往成为开启编程之旅的第一道门槛。在这篇文章中,我们将引领您打开一扇通往C++编程新境界的大门。让我们一起踏上这场充满挑战与收获的编程之旅。 1.包阅AI:编程学习的私人定制 官网直达链接:https://baoyueai.comhttps://www.douban.com/note/865294815/
8.C++语言编程C++在线编程平台,智能编程,AI编程,人工智能编程,在线运行C++程序,C++教学,大数据分析,支持运行参数和输入输出交互。https://ptms.zhetao.com/ptms.oms?platform=cplusplus
9.C++虽然在标准中增加STL是个很重要的决定,但也因此延缓了C++标准化的进程。 委员会于1997年11月14日通过了该标准的最终草案,1998年,C++的ANSI/IS0标准被投入使用。通常,这个版本的C++被认为是标准C++。所有的主流C++编译器都支持这个版本的C++,包括微软的Visual C++和Borland公司的C++Builder。 [3-6] C++编程开发https://baike.baidu.com/item/C++/99272
10.硬核!C++并发编程(C++11到C++17)腾讯云开发者社区由此,掌握并发编程技术,利用多处理器来提升软件项目的性能将是软件工程师的一项基本技能。 本文以C++语言为例,讲解如何进行并发编程。并尽可能涉及C++11,C++14以及C++17中的主要内容。 并发与并行 并发(Concurrent)与并行(Parallel)都是很常见的术语。 Erlang之父Joe Armstrong曾经以人们使用咖啡机的场景为例描述了这https://cloud.tencent.com/developer/article/1876855
11.c++视频教程c++教程pdfvisualc++教程pdfc++教程是专门为C++编程人员准备的学习资料,适合于任何开发环境,可以在任何操作系统或程序开发系统中使用,无需基础就能轻松的学校c++的知识,帮助你从根本上理解和掌握c++的相关知识,感兴趣的朋友可以来绿色资源网下载!http://www.downcc.com/k/cppjiaocheng/
12.少儿编程C++语言介绍今天我们介绍的是少儿编程体系当中的C++语言。C++语言可以说是最经典的编程语言,基本上大学里面的计算机相关专业,C++语言是必学的一门编程语言。 C++语言是什么? 在中国,“C++”常被称为“C加加”,是一种应用非常广泛的计算机程序设计语言。他从C语言发展而来,保留了C语言原有的所有优点,并添加了面向对象的机制。https://www.youkee.com/ziliao/15061.html
13.C++语言程序设计进阶清华大学而C++语言是应用最广泛的面向对象的程序设计语言之一。 本课程是一门面向广大初学者的入门课程,自1999年开始在清华大学开设,本课程的教材已经在清华大学等一百多所学校的不同专业中使用,取得了良好的教学效果。 本课程将C++语言作为大学生的计算机编程入门语言,不仅详细介绍语言本身,而且介绍常用的数据结构和算法、面向https://www.xuetangx.com/course/THU08091000248/10318294
14.C语言中文网:C语言程序设计门户网站(入门教程编程软件)C语言中文网是中国领先的C语言程序设计专业网站,提供C语言入门经典教程、C语言编译器、C语言函数手册,C语言编程技巧,C语言考试试题等,是学习、自学C语言程序设计的好帮手。https://c.biancheng.net/
15.C++游戏编程入门第4版.pdfC++游戏编程入门第4版:书籍类别:C/C++/C#购买链接:京东异步社区网友评分:应用平台:PDFC++游戏编程入门(第4版)从游戏编程的角度介绍C++语言,既独具匠心又妙趣横生。下载前务必先预览,自己验证一下是不是你要下载的文档! 封面 书名 版权 前言 目录 第1章 类型、变量与标准I/O:Lost Fortune/1 1.1 C++https://max.book118.com/html/2019/0404/7121033062002016.shtm
16.C++教程编程入门教程C++ 标识符是用来标识变量、函数、类、模块,或任何其他用户自定义项目的名称。一个标识符以字母 A-Z 或 a-z 或下划线 _ 开始,后跟零个或多个字母、下划线和数字(0-9)。C++ 标识符内不允许出现标点字符,比如 @、& 和 %。C++ 是区分大小写的编程语言。因此,在 C++ 中,Manpower 和manpower 是两个不同https://www.nowcoder.com/tutorial/10003/e546205d358e46668418c0a38840051f
17.C++游戏编程入门(第4版)(道森著)中文pdf扫描版[56MB]电子书下载☉ 如果遇到什么问题,请评论留言,我们定会解决问题,谢谢大家支持! ☉ 本站提供的一些商业软件是供学习研究之用,如用于商业用途,请购买正版。 ☉ 本站提供的C++游戏编程入门(第4版) (道森著) 中文pdf扫描版[56MB]资源来源互联网,版权归该下载资源的合法拥有者所有。https://www.jb51.net/books/633165.html
18.C++入门教程(配套编程题库)这套《C++入门教程》由站长黄老师亲自撰写和设计,面向有C语言基础的同学,如果还没有学习过C语言可以点击这里先学习C语言教程。 C++课程配套的编译器采用CodeBlocks,也可以继续使用VC6编译器及其他编译器,本教程配套使用编译器下载地址见:CodeBlocks使用教程及下载地址。 https://www.dotcpp.com/course/cpp/
19.C++语言参考MicrosoftLearn本参考将介绍在 Microsoft C++ 编译器中实现的 C++ 编程语言。 本文的结构基于 Margaret Ellis 和 Bjarne Stroustrup 撰写的《C++ 参考手册批注》和 ANSI/ISO C++ 国际标准 (ISO/IEC FDIS 14882)。 本文涵盖了 C++ 语言功能的 Microsoft 专用实现。 有关新式 C++ 编程做法的概述,请参阅欢迎回到 C++。 https://docs.microsoft.com/zh-cn/cpp/cpp/cpp-language-reference
20.c++编程教程5. Who developed C++ programming language? Bjarne Stroustrup developed C++ programming as an extension to the C language. C++ can be considered an advanced version of the C language with object-oriented concepts. 6. What was the original name of C++? https://www.tutorialspoint.com/cplusplus/index.htm