Modern C++ 学习笔记——C++函数式编程

article/2025/10/19 22:02:01

往期精彩:

  • Modern C++ 学习笔记——易用性改进篇
  • Modern C++ 学习笔记 —— 右值、移动篇
  • Modern C++ 学习笔记 —— 智能指针篇
  • Modern C++ 学习笔记 —— lambda表达式篇
  • Modern C++ 学习笔记 —— C++面向对象编程
  • Modern C++ 学习笔记 —— C++函数式编程

Modern C++ 学习笔记——C++函数式编程

关键字:lambda表达式、函数式编程

文章目录

  • Modern C++ 学习笔记——C++函数式编程
    • 函数式编程
      • 什么是函数式编程?
      • 函数式编程
      • 函数式编程的特点
    • 函数式编程用到的技术
      • first class function(头等函数)
      • map & reduce & filter
        • map
        • reduce
        • filter
      • pipeline(管道)
      • currying (柯里化)
      • recursing & tail recursion optimization (递归&尾递归优化)

函数式编程

在之前的系列文章中介绍过函数对象和lambda表达式,本篇文章就来讲讲它们的主要用途——函数式编程。

什么是函数式编程?

那什么是函数编程呢?它其实来自于数学中的理念.

f(x) = 2x^2 +x+3
g(x) = 3f(x) + 5 = 6x^2 + 3x + 14
h(x) = f(x) + g(x) = 8x^2 + 4x + 17

正如上面的数学函数一样,对于函数式编程,它只关心于定义输入数据和输出数据的关系,在数学表达式中我们称其为输入与输出的一种映射(map),即用函数定义输入数据和输出数据的关系是什么样的。
这样就可以得到关于函数式编程一下特点:

  • 无状态:函数不维护任何状态。函数式编程的核心精神是stateless
  • 不可变数据输入数据不可变,动了输入数据就有危险,所以要返回新的数据集。
    或许这么说有点干燥,为了更好的帮助理解,再举一个最简单的例子:
int copy_add(int x, int y)
{return x + y;
}
int nocopy_add(int& x, int& y)
{x += y;return x;
}

以上两个函数都实现了对输入的两个int类型值进行相加并且返回的功能,第一个函数式比较纯粹的函数,符合我们上面所说的函数式编程的特点。再看第二个函数,它将入参设为引用(在项目中,为了减少值对象拷贝的性能消耗,常把入参设为引用),这就带来了问题——入参很容易在函数内部被改变。
在著作《Functional Programming in C++》(非常推荐此书)中给出了关于函数式编程的定义:

Functional programming is a style of programming that emphasizes the evaluation of expressions, rather than execution of commands. The expressions in these languages are formed by using functions to combine basic values. A functional language is a language that supports and encourages programming in a functional style.

简单来说:在OOP(面向对象编程)中,正如大多数人做的那样,更多的考虑是算法的步骤,即对对象的处理。而在函数式编程里你需要学会如下的思考方式:什么是输入,什么是输出,还有需要执行哪些转换将两者映射起来

函数式编程

在介绍了函数编程之后,我们尝试使用其思想来解决实际的问题:假如有个vector保存着文件中的所有单词,需要统计其所有单词出现的频率,并且按照评率高低输出对应单词。最传统的命令式编程大概会这么写:

void print_word(vector<string>& words) {unordered_map<string, int> wordCount;for (auto&& s : words) {unordered_map<string, int>::iterator iter = wordCount.find(s);if (iter == wordCount.end()) {wordCount.insert({s, 1});} else {wordCount[s]++;}}vector<pair<int, string>> reverseword;for (auto it = wordCount.begin(); it != wordCount.end(); ++it) {reverseword.emplace_back(make_pair(it->second, it->first));}sort(reverseword.begin(), reverseword.end(), [](const pair<int, string>& lhs, const pair<int, string>& rhs) {return lhs.first > rhs.first;}); // 高阶函数for (auto& p : reverseword) {cout << "word is " << p.second << ", count is " << p.first << endl; }
}

为完成所需要的功能,主要由以下过程组成:

  • 将所有的单词插入unordered_map<string, int>中,并统计出现的次数(unordered_map要比map更快些)
    vector< string> -> unordered_map<string, int>
  • 将unordered_map<string, int>中的键值对转换为pair<int, string>并存入vector
    unordered_map<string, int> ->vector<pair<int, string>>
  • 对所有pair<int, string>排序
    vector<pair<int, string>> -> vector<pair<int, string>>
  • 遍历vector,输出单词和统计次数
    vector<pair<int, string>> -> void

在这里插入图片描述

可以看出我们可以拆为更小的函数(做更简单的事),然后将他们组合起来:

unordered_map<string, int> count_occurrencese(vector<string> items);
vector<pair<int, string>> reverse_pairs(unordered_map<string, int> pairs);
vector<pair<int, string>> sort_by_frequency(vector<pair<int, string>>);
void print_pairs(vector<pair<int, string>> pairs);

是不是发现了什么端倪?是的,我们可以通过组合的方式完成上面命令式代码同样的功能。

void print_words(vector<string>& words)
{return print_pairs(sort_by_frequency(reverse_pairs(count_occurrencese(words)));
}

或许你会觉得这也没啥嘛,之前的代码不也是按照如此的算法过程实现的。那么我们考虑如下场景:当我的输入不在是vector,而是一个文件,统计其中单词频率并输出,我们只需要在声明一个函数words: file -> vector<string>完成文件到单词的映射,之后与上面类似进行组合即可,而不用再对print_words进行修改。当输入变成已统计好的unordered_map时候,也同理。
我们把之前过程是编程范式叫做——指令式编程,而把函数式编程范式叫做——声明式编程。可不要小瞧这一个思维的转变,它带来的变化可谓是相当大的。还是上面的例子,假如我们对其使用一些C++中的语法糖,你会发现是如此甜蜜:

template <typename C, typename T = typename C::value_type>
std::unordered_map<T, unsigned int> count_occurrences(const C& collection)
template <typename C, typename P1, typename P2>
std::vector<std::pair<P2, P1>> reverse_pairs(const C& collection);

在上面的声明中,函数count_occurrences变得将能够接受任何集合,只要其能够推断其包含项的类型(C::value_type)。从此它不再局限于我们上面需求中,你可以用来来统计字符串中字符、整数列表中的整数值、字符串集合中的字符串等等。其他声明的函数也可以做类似的扩展。

函数式编程的特点

函数式编程期望函数的行为项数学上的函数,而非一个计算机上的“子程序”。这样的函数一般被称为纯函数(pure function),主要体现在确定性。所谓确定性,就是像数学中那样,f(x) = y 这个函数无论什么场景都会得到同样的结果。而不是像程序中的很多函数那样。同一个参数,在不同的场景下会计算出相同的结果,这个我们称之为函数的确定性。所谓不同的场景,就是我们的函数会根据运行中的状态信息的不同而发生变化。

我们的代码也体现了函数式编程的一些特点:

  • 会影响函数结果的只是函数的参数,没有对环境的依赖。
  • 返回的结果就是函数执行的唯一结果,不产生对环境的其他影响。
  • 函数的执行没有顺序上的问题
  • 函数可以像普通的对象一样被传递、使用或返回。
  • 代码更像是说明式而非命令式。熟悉函数式编程后,你会发现说明式代码的可读性比命令式更高,代码更短,可复用性更高。
  • 无状态,没有状态就没有伤害,就像没有依赖就没有伤害一样。

函数式编程用到的技术

first class function(头等函数)

正如前文所述,在函数式编程中函数就如同对象一样,可以被传递、使用、或返回。而这些函数被称为头等函数, 也有人将函数式编程中的函数称为一等公民。
在C++中可以做到这一点的有函数对象,lambda表达式(推荐阅读[lambda表达式篇](https://blog.csdn.net/weixin_43077022/article/details/117926275?spm=1001.2014.3001.5501),此外还有std::functionstd::bind等。

class filter{ // 函数对象
public:students() {names.insert("abc");names.insert("John");}    bool operator()(std::string name) {return names.find(name) == names.end();}
private:set<std::string> names;
};
// lambda 表达式,用auto捕获匿名函数对象
auto add2 = [](int x) {	return x + 2; }

map & reduce & filter

在函数式编程很多函数已称为了基本的惯用法(在不同语言有不同名字),而**map(映射)、reduce(归并)和filter(过滤)**为其中最为常见也是最为基础的三个。

map

Map在C++中的直接映射是transform(头文件< algorithm>)。他所做的事情也是数学上的映射,把一个范围里的对象转换为相同数量的另外一些对象。假如有类person,我需要获得人到姓名的映射,即vector<person> -> vector<string>

struct person;
vector<string> GetNames(vector<person> people)
{vector<string> names;transform(people.begin(), people.end(), back_inserter(names), [](const person& tmp) {return tmp.name;});return names;
}

std::back_inserter是定义在头文件 <iterator>中,用于构造 std::back_insert_iterator 的便利函数模板[3].

reduce

Reduce在C++中的直接映射是accumulate(头文件< numeric>)。它的功能是在指定的范围内,使用给定的出事和函数对象,从左到右对数值进行归并[4]。看两个计算平均值的写法:

double average_score_1(const vector<int>& scores)
{int sum = 0;for (int socre : scores) {sum += socre;}return sum / (double)scores.size();
}double average_score_2(const vector<int>& scores)
{return accumulate(scores.begin(), scores.end(), 0) / (double)scores.size();
}

此外,还可以提供第四个参数用于其他计算,例如如下代码实现累乘:

	int product = std::accumulate(v.begin(), v.end(), 1, std::multiplies<int>()); 

上述的代码可以显而易见的得出,比起过程式的语言来说,函数式编程在代码上要更容易阅读。(传统过程式的语言需要使用for/while循环,然后在各种变量中把数据倒过来倒过去的)。此外,再考虑我们之前说到的函数是无状态的,这意味着并行无问题,尤其在本例中明显。在面临大量的数据时,函数式编程能够提供并行性。而在C++17引入了std::reduce[5],以及执行策略[6]让其并行计算成为可能。

int main()
{std::vector<double> v(10'000'007, 0.5);{auto t1 = std::chrono::high_resolution_clock::now();double result = std::accumulate(v.begin(), v.end(), 0.0);auto t2 = std::chrono::high_resolution_clock::now();std::chrono::duration<double, std::milli> ms = t2 - t1;std::cout << std::fixed << "std::accumulate result " << result<< " took " << ms.count() << " ms\n";}{auto t1 = std::chrono::high_resolution_clock::now();double result = std::reduce(std::execution::par, v.begin(), v.end());auto t2 = std::chrono::high_resolution_clock::now();std::chrono::duration<double, std::milli> ms = t2 - t1;std::cout << "std::reduce result "<< result << " took " << ms.count() << " ms\n";}
}

可能的输出:

std::accumulate result 5000003.50000 took 12.7365 ms
std::reduce result 5000003.50000 took 5.06423 ms

filter

Filter的功能是进行过滤,筛选出符合条件的成员。在C++中的映射有copy_if和partition。

auto is_female = [](const person& tmp) { return tmp.female; };
auto iter = std::partition(people.begin(), people.end(), is_female);vector<person> females;
std::copy_if(people.cbegin(), people.cend(), std::back_inserter(females), is_female);

pipeline(管道)

该技术的意思是,将函数实例成一个一个的action,然后将一组 action 放到一个数组或是列表中,再把数据传给这个 action list,数据就像一个 pipeline 一样顺序地被各个函数所操作,最终得到我们想要的结果。正如在前文组装函数那样。

void print_words(vector<string>& words)
{return print_pairs(sort_by_frequency(reverse_pairs(count_occurrencese(words)));
}

pipeline 管道借鉴于Unix Shell的管道操作——把若干个命令串起来,前面命令的输出成为后面命令的输入,如此完成一个流式计算。(注:管道绝对是一个伟大的发明,他的设哲学就是KISS – 让每个功能就做一件事,并把这件事做到极致,软件或程序的拼装会变得更为简单和直观。)
比如shell命令:

ps auwwx | awk '{print $2}' | sort -n | xargs echo

查看一个用户执行的进程列表,列出来以后,然后取第二列,第二列是它的进程 ID,排个序,再把它显示出来。

C++20引入范围库(ranges)之后,可以使用operator |链接两个范围适配器闭包对象的结果。而在此之前我们可以尝试重载管道符,达到类似的效果。以下例子仅为了说明该项技术。

template<typename T, typename F>
auto operator | (T t, F f) -> T
{return f(t);
}
auto f = [](const int& a) {return a + 1;};
auto g = [](const int& a) {return a * 2;};
auto h = [](const int& a) {return a - 1;};
auto y = 3 | h | g | f;

currying (柯里化)

将一个函数的多个参数分解成多个函数,然后将函数多层封装起来,每层函数都返回一个函数去接收下一个参数,这可以简化函数的多个参数。简单点说就是函数到函数。

auto addThree = [](int x, int y, int z){return x + y + z; };
auto addTwoToOne = [addThree](int x, int y) {return [=](int z) {return addThree(x, y, z);};
};
auto addOneToTwo = [addThree](int x) {return [=](int y, int z) {return addThree(x, y, z);};
};
auto addOneByOne = [addTwoToOne](int x) {return [=](int y) {return addTwoToOne(x, y);};
};cout << "addThree = " << addThree(1, 2, 3) <<endl;
cout << "addTwoToone = " << addTwoToOne(1, 2)(3) <<endl;
cout << "addOneToTwo = " << addOneByOne(1)(2)(4) <<endl;

在上面的代码中addThree函数实现了对三个int值进行相加的操作。然后对该函数进行了拆分,将其拆为addTwoToOne,进而拆为addOneToOne,在调用的时候就变成了
addOneByOne(1)(2)(4),而这个过程就被称作currying(柯里化)。
我们上面做的那个函数拆解也正是此意。
在这里插入图片描述

recursing & tail recursion optimization (递归&尾递归优化)

递归最大的好处就是简化代码,可以把一个复杂问题用很简单的代码描述出来。(注意:递归的精髓是描述问题,这也是函数是编程的精髓)。
我们也知道递归的危害,那就是如果递归很深的话,stack受不了,并会导致性能大幅度下降。因此,我们使用尾递归优化技术——每次递归时都会重用stack,这样能够提升性能。或许Stack Overflow上的这篇问答能够帮你解释What is tail call optimization?
C++ 标准库并不保证尾递归优化能够执行,但是主流的C++编译器(如GCC\Clang\MVSC)都是支持尾递归优化的。

[1]《Functional Programming in C++》
https://www.manning.com/books/functional-programming-in-c-plus-plus

[2]https://zh.cppreference.com/w/cpp/algorithm/transform

[3]https://zh.cppreference.com/w/cpp/iterator/back_inserter

[4]https://zh.cppreference.com/w/cpp/algorithm/accumulate

[5]https://zh.cppreference.com/w/cpp/algorithm/reduce

[6]https://zh.cppreference.com/w/cpp/algorithm/execution_policy_tag_t

[7]https://zh.cppreference.com/w/cpp/algorithm/partition

[8]https://zh.cppreference.com/w/cpp/ranges

[9]How can currying be done in C++?
https://stackoverflow.com/questions/152005/how-can-currying-be-done-in-c

[10]https://coolshell.cn/articles/10822.html


http://chatgpt.dhexx.cn/article/MiYAKcl4.shtml

相关文章

java8函数式编程实例

什么是函数式编程 函数式编程是java8的一大特色&#xff0c;也就是将函数作为一个参数传递给指定方法。别人传的要么是基本数据类型&#xff0c;要么就是地址引用 &#xff0c;我们要穿一个“动作”。 Stream 说到函数式编程&#xff0c;就不得不提及Stream&#xff0c;Stre…

Scala函数式编程

一、函数式编程定义&#xff1a; Scala是一门既面向对象&#xff0c;又面向过程的语言。在Scala中&#xff0c;函数与类、对象地位是一样&#xff0c;所以说scala的面向过程其实就重在针对函数的编程 了&#xff0c;所以称之为函数式编程 在Scala中定义函数需要使用 def 关键…

什么是函数式编程?

当我们说起函数式编程来说&#xff0c;我们会看到如下函数式编程的长相&#xff1a; 函数式编程的三大特性&#xff1a; immutable data 不可变数据&#xff1a;像Clojure一样&#xff0c;默认上变量是不可变的&#xff0c;如果你要改变变量&#xff0c;你需要把变量copy出去修…

python函数式编程

大家好 这里还还还是长弓 今天我们来讲讲python中的函数式编程 目录 函数式编程 高阶函数 map reduce filter sorted 返回函数 闭包 nonlocal使用 匿名函数lambda 装饰器 偏函数 函数式编程 有些同学疑惑了&#xff0c;我们已经学了函数&#xff0c;为什么还要学这…

函数式编程

Functional Programming 什么是函数式编程 函数式编程的思维方式&#xff1a;把显示世界的事务和事物之间的联系抽象到程序世界&#xff08;对运算过程进行抽象&#xff09; 函数式编程中的函数指的数学中的函数即映射关系&#xff0c;输入的值对应一个输出的值&#xff0c;…

appium环境搭建python_appium环境搭建python

1&#xff0c;appium是开源的移动端自动化测试框架&#xff1b;2&#xff0c;appium可以测试原生的、混合的、以及移动端的web项目&#xff1b;3&#xff0c;appium可以测试ios&#xff0c;android应用(当然了&#xff0c;还有firefox os)&#xff1b;4&#xff0c;appium是跨平…

Windows下Appium环境搭建小结

文章目录 Windows下Appium环境搭建小结需要安装的软件1. JDK下载安装/配置 2. Android SDK3. Maven下载安装/配置 4. Appium下载安装/配置 5. Eclipse TestNG 和 ADT 插件下载安装一条龙配置1、先配置Maven 创建一个项目 Windows下Appium环境搭建小结 本文需要读者已安装了Ec…

Mac端Python+Appium环境搭建

一、安装java sdk java安装&#xff1a;下载完直接安装jdk1.8 二、 安装Android Studio 1.下载安装 下载地址&#xff1a;https://www.androiddevtools.cn/# 2.安装完成后&#xff0c;打开SDK Manager 三、JAVA SDK和Android SDK环境变量配置 1.终端输入&#xff1a;ls…

安卓移动端appium环境搭建流程

安卓移动端appium环境搭建流程 基本步骤: 安装Node.js 安装JDK&#xff0c;及配置环境变量 安装SDK&#xff0c;及配置环境变量 安装Appium桌面版本(建议安装GitHub的最新版) python中pip下载Appium-Python-Client 下载allure-2.13.8并加入环境变量 管理员身份运行appiu…

pythonappium环境搭建_python+appium 环境搭建

最近学习了一下python语言&#xff0c;听说appium是做app的ui层的自动化的一个很好的框架&#xff0c;也是很多人在学习的框架&#xff0c;所以很感兴趣&#xff0c;也特意来学习一下&#xff0c;下面是我学习过程的一些心得和总结&#xff0c;希望对大家有所帮助。 一、环境搭…

Appium环境搭建(集齐Windows和MacOS的宝藏内容)

Appium环境搭建目录 Windows系统环境下安装Node.js安装JDK及环境变量配置添加环境变量 安装SDK添加环境变量 安装Appium可通过三种方法安装安装 **appium-doctor** MacOS系统环境下安装xcode安装依赖安装WebDriverAgent运行WebDriverAgent windows 安装 tidevice常用的tidevice…

mac appium环境搭建

appium环境的搭建其实也不复杂&#xff0c;主要是配置的比较多&#xff0c;只是在配置的过程中&#xff0c;根据当时的机器配置会遇到一些具体问题&#xff0c;一个个解决就可以了。 安装下面这篇文章搭建就可以了 超详细的Mac下appium环境搭建 配置java环境有问题&#xff0c;…

pythonappium环境搭建_python appium环境搭建

1&#xff0c;appium是开源的移动端自动化测试框架&#xff1b; 2&#xff0c;appium可以测试原生的、混合的、以及移动端的web项目&#xff1b; 3&#xff0c;appium可以测试ios&#xff0c;android应用&#xff08;当然了&#xff0c;还有firefox os&#xff09;&#xff1…

Appium 环境搭建

一、下载并安装appium客户端(勿装1.15.1版本,1.15.1版本很多坑) 进入appium官网http://appium.io/下载版本&#xff0c;将下载好的版本按照步骤进行安装 Appium-Python-Client第三方包 pip3 install Appium-Python-Client -i https://pypi.tuna.tsinghua.edu.cn/simple 二…

appium环境搭建全套

环境 1 Node.js 2 Appium 3 Appium-desktop 4 Appium-Python-Client 5 Python 6 JDK 7 Andriod SDK 8 Appium-doctor 一、安装Node.js 下载地址&#xff1a;https://nodejs.org/en/download/releases/ 注意&#xff1a;Appium版本是1.7.2&#xff0c;则选择的No…

Appium环境搭建

一、Appium框架原理 1.介绍 appium是一个移动端的自动化测试框架&#xff0c;可用于测试原生应用&#xff0c;移动网页应用和混合应用&#xff0c;支持iOS和Android。 2.原理 appium可以理解为一个c/s架构软件&#xff0c;在pc端安装的appium server端&#xff0c;通过appi…

Appium环境搭建教程

最近打算研究开发一个手机的自动化小工具&#xff0c;奈何在这方面自己是一个小白&#xff0c;于是开始针对手机进行研究。由于主要使用Appium这个工具&#xff0c;因此本文主要讲解Appium环境的搭建&#xff0c;并结合自己的实践讲一讲需要避过的坑。 一、 安装Node.js Node.…

MySQL函数语句

目录 一、MySQL数据库函数作用二、MySQL数据库函数分类1.1.1、数学函数常用的数学函数1、abs(x)&#xff1a;返回x的绝对值2、rand() &#xff1a;返回0到1的随机数3、mod(x&#xff0c; y) &#xff1a;返回x除以y以后的余数4、power(x&#xff0c; y)“&#xff1a;返回x的y次…

MySQL函数(=)

1 将username字段的截取两个字符&#xff0c;其中将包含为1的字符替换为q SELECT REPLACE(SUBSTRING(username,1,2),1,q) FROM guanliyuan; 2 将日期时间转换为字符串 SELECT DATE_FORMAT(2009-10-11 22:12:12,%Y%m%d%H%i%s); 3 从日期中截取年份 SELECT SUBSTRING(DATE_FO…

MySQL函数介绍

MySQL数据库提供了很多函数包括&#xff1a; 数学函数&#xff1b;字符串函数&#xff1b;日期和时间函数&#xff1b;条件判断函数&#xff1b;系统信息函数&#xff1b;加密函数&#xff1b;格式化函数&#xff1b; 一、数学函数 数学函数主要用于处理数字&#xff0c;包括整…