闭关之 C++ 函数式编程笔记(一):函数式编程与函数对象
目录
- 前言
- 第一章 函数式编程简介
- 1.1 命令式与声明式编程比较
- 1.2 纯函数(Pure functions)
- 1.2.1 避免可变状态
- 1.3 以函数方式思考问题
- 1.4 函数式编程的优点
- 1.4.1 代码简洁易读
- 1.4.2 并发和同步
- 1.4.3 持续优化
- 1.5 C++ 作为函数式编程语言的进化
- 第二章 函数式编程之旅
- 2.1 函数使用函数?
- 2.2 STL 实例
- 2.2.1 求平均值
- 2.2.2 折叠 (Folding)
- 2.2.3 删除字符串空白符
- 2.2.4 基于谓词分割集合
- 2.2.5 过滤(Filtering)和转换(Transforming)
- 2.3 STL 算法的可组合性和使用性
- 总结
- 关键 API
- 第三章 函数对象
- 3.1 函数和函数对象
- 3.1.1 自动推断返回值类型
- 3.1.2 函数指针
- 3.1.3 调用操作符重载
- 3.1.4 创建通用函数对象
- 3.2 lambda 和闭包 (Closure)
- 3.2.1 lambda 语法
- 3.2.2 lambda 详解
- 3.2.3 在 lambda 中创建任意成员变量
- 3.2.4 通用 lambda 表达式
- 3.3 编写比 lambda 更简洁的函数对象
- 3.3.1 STL 中的操作符函数对象
- 3.4 用 std::function 包装函数对象
- 总结
- 关键 API
前言
C++ 20 也已经了解一段时间了。自己的感觉是:大佬们在推函数式编程和模板元编程。本来自己是 Java 出身,想深入OOP。但是,现在看来,有必要深入了解一下函数式和模板元编程了。先从函数式编程开始。《C++ 函数式编程》正好香雪馆有这本书,近水楼台,就不买了!
- 官方代码:https://www.manning.com/books/functional-programming-in-c-plus-plus
- 官方代码笔记: https://gitee.com/thebigapple/Study_CPlusPlus_20_For_CG.git
第一章 函数式编程简介
1.1 命令式与声明式编程比较
函数式编程
- FP (functional programming)
- 强调表达式求值,而不是指令的执行
- 这些表达式由函数和基本的值组合而成
命令式实现统计多个文件的行数
std::vector<int> count_lines_in_files_command(const std::vector<std::string>& files) {std::vector<int> results;char c = 0;for (const auto& file : files) {int line_count = 0;std::ifstream in(file);while (in.get(c)) {if (c == '\n') {line_count++;}}results.push_back(line_count);}return results; }
声明式实现统计多个文件的行数
int count_lines(const std::string& filename) {std::ifstream in(filename);in.unsetf(std::ios_base::skipws);return (int) std::count(std::istream_iterator<char>(in),std::istream_iterator<char>(),'\n'); }std::vector<int> count_lines_in_files(const std::vector<std::string>& files) {std::vector<int> results;for (const auto& file : files) {results.push_back(count_lines(file));}return results; }
上述声明式实现的优势
- 不需要关心统计是如何进行的,只需要说明在给定的流中统计行数的数目就行
函数式编程的主要思想
- 使用抽象来表述用户的目的(Intent)
- 而不是说明如何去做
使用 std::transform 进一步抽象
std::vector<int> count_lines_in_files_transform(const std::vector<std::string>& files) {std::vector<int> results(files.size());std::transform(files.cbegin(), files.cend(),results.begin(),count_lines);return results; }
- transform 前两个参数指定内容,第三个参数指定存放起始位置,第四个参数指定转换函数
通过 range 和 range transformations 再次抽象
std::vector<int> count_lines_in_files_range(const std::vector<std::string>& files) { //使用 | 管道操作符表示通过 transform 传递一个集合auto view = files | std::views::transform(count_lines) ;return { std::ranges::begin(view), std::ranges::end(view) };; }
1.2 纯函数(Pure functions)
- FP的核心思想是纯函数
- 函数只使用(而不修改)传递给它的实际参数计算结果
- 如果使用相同的实参多次调用纯函数,将得到相同的结果,并不会留下调用痕迹(无副作用)
- 纯函数不能修改程序的状态
- 因此,利用真正意义上的纯函数,开发程序就毫无意义
- 纯函数重定义 (放松要求)
- 任何(除了硬件以上的层)没有可见副作用的函数称为纯函数
- 纯函数的调用者除了接收它的返回结果外,看不到任何它执行的痕迹
- 不提倡只使用纯函数,而是限制非纯函数的数量
1.2.1 避免可变状态
- 不纯的第一个方面
- 是不是外部可见的
- 变量是局部变量就对外不可见
- 是不是外部可见的
- 思维路线
- 第一步,判断函数内的变量是不是外部可见。
- 全部都是局部变量对外不可见 (伪纯)
- 第二步,将不纯的地方移到其他函数(局部变量)
- 第三步,判断是否还有局部状态
- 没有可变状态也没有不可变状态,即是一个FP风格代码
- Code_1_1_1
- 第一步,判断函数内的变量是不是外部可见。
1.3 以函数方式思考问题
- 利用 1.1.2 的思维路线编写代码是低效的
- 换一种方式思考问题
- 考虑输入是什么,输出是什么
- 从输入到输出需要什么样的转换
- 而不是去思考算法的步骤
- 思想
- 把一个大的问题分解成小的问题、独立的任务,并且方便地把他们组合起来
1.4 函数式编程的优点
1.4.1 代码简洁易读
1.4.2 并发和同步
- C++ 编译器在检测到循环体是“纯”的时,可以自动进行向量化或进行其他的优化
- 这种优化会对标准代码产生影响
- 因为标准算法的内部是通过循环实现的
- 这种优化会对标准代码产生影响
1.4.3 持续优化
- 使用抽象层更高的STL或其他可信库函数的优点
- 即使不修改任何一行代码,程序也在不断的提高性能
- 很多程序员倾向于手动编写低层次关键性能代码
- 但这种优化只针对特定的平台
- 阻碍了编译器对其他平台代码的优化
1.5 C++ 作为函数式编程语言的进化
- 泛型编程思想
- 可以编写通用概念的代码,并可以把它应用于适合这些概念的任意结构
第二章 函数式编程之旅
2.1 函数使用函数?
- 高阶函数(higher-order function)
- 能够接收函数作为参数或返回函数作为结果的函数称为高阶函数
- 高阶结构
- 过滤结构
- 接收一个集合和一个谓词函数
(T -> bool)
作为参数,并返回一个过滤后的集合 - 写作:
filter: (collection<T>, (T -> bool)) -> cllection<T>
- 接收一个集合和一个谓词函数
- 映射 (map) 或转换 (transform)结构
- 对集合中的每一个元素调用转换函数,并收集结果到新集合中
- 写作:
transform: (collection<In>, (In -> Out)) -> collection<Out>
- 过滤结构
- 过滤和转换是通用的编程模式
2.2 STL 实例
2.2.1 求平均值
- 高阶函数
std::accumulate()
- 参数:一个集合和一个初始值, 第三个参数可以提供一个自定义函数(可以不提供)
- 返回初始值和集合中所有元素的累加和
- 算法比较特别
- 它保证集合中每个元素逐个累加,这使得不改变其行为的情况下不可能将它并行化
- 如果要并行累加所有的元素,可以使用
std::reduce()
- 可以把
std::multiplies<>()
作为最后一个参数将算法改为求乘积
- Code
auto average_score(const std::vector<int>& scores) -> double {return std::accumulate(scores.cbegin(), scores.cend(),0) / (double)scores.size(); }
auto average_score(const std::vector<int>& scores) -> double {return std::reduce(std::execution::par,scores.cbegin(), scores.cend(),0) / (double)scores.size(); }
auto product_score(const std::vector<int>& scores) -> double {return std::accumulate(scores.cbegin(), scores.cend(),1,std::multiplies<int>()); }
2.2.2 折叠 (Folding)
std::accumulate()
是折叠的一种实现- 折叠提供了对递归结构,如向量、列表和树的遍历处理,并允许逐步构建自己需要的结果
- 折叠接收一个集合,其中包含 T 类型的条目、R 类型的初始值 (没必要与 T 是相同类型) 和一个函数
f: (R, T) -> R
- 对初始值和集合的第一个元素调用给定的函数
f
- 结果与集合中第二个元素再传递给函数
f
. - 一直重复到集合中所有元素处理完毕
- 算法返回一个 R 类型的值
- 这个值是函数
f
最后一次调用的返回值
- 这个值是函数
- 对初始值和集合的第一个元素调用给定的函数
- 从第一个元素开始处理称为左折叠
- 从集合的最后一个元素开始处理,称右折叠
- C++ 没有提供右折叠算法
- 可以传递反向迭代器实现右折叠效果
crbegin 和 crend
2.2.3 删除字符串空白符
std::find_if()
- 它查找集合中第一个满足指定谓词的元素
- Code
auto trim_left(std::string s) -> std::string {s.erase(s.begin(),std::find_if(s.begin(), s.end(), is_not_space));return s; }auto trim_right(std::string s) -> std::string {s.erase(std::find_if(s.rbegin(), s.rend(), is_not_space).base(),s.end());return s; }auto trim(std::string s) -> std::string {return trim_left(trim_right(std::move(s))); }
2.2.4 基于谓词分割集合
std::partition()
和std::stable_partition()
- 都接收一个集合和一个谓词
- 他们对原集合中的元素进行重排,把符合条件的与不符合条件的元素分开
- 符合谓词条件的元素移动到集合的前面
- 不符合谓词条件的元素移动到集合的后面
- 算法返回一个迭代器,指向第二部分的第一个元素
- 不符合谓词条件的第一个元素
- 返回的迭代器和原集合首端迭代器配合,获取满足谓词条件的元素
- 返回的迭代器与原集合尾端迭代器配合,获取原集合中不符合谓词条件的元素
- 即使这些集合中存在空集也是正确的
- 两个算法的区别
std::stable_partition()
可以保持集合中原来的顺序
- Code_2_2_4
2.2.5 过滤(Filtering)和转换(Transforming)
std::remove_if()
和std::remove()
- 删除集合中满足谓词或包含特定值的函数
- 但是这个函数是 erase-remove 风格
- erase-remove 风格
- 算法只把不符合删除条件的元素移动到集合的开头部分
- 其他元素则是不确定的状态,算法返回一个指向第一个这样元素的迭代器
- 如果没有元素被删除,则指向集合的末尾
- 需要把这个迭代器传递给集合的成员
erase()
,由它来执行people.erase(std::remove_if(people.begin(), people.end(),is_not_female),people.end());
- 如果不想改变原来的集合可以使用
std::copy_if()
- 前两个参数为待复制的源集合迭代器
- 第三个参数为存放复制元素的集合起始迭代器
- 使用
std::back_inserter()
包裹目标集合即可
- 使用
- 最后一个参数为筛选谓词
std::copy_if(people.cbegin(), people.cend(),std::back_inserter(females),is_female);
std::transform()
转换函数- 参数
- 前两个参数为待转换的源集合迭代器
- 第三个参数为存放转换结果集合的起始迭代器
- 最后一个参数是转换函数
std::transform(females.cbegin(), females.cend(),names.begin(),name);
- 参数
2.3 STL 算法的可组合性和使用性
- STL 算法组合效率不高
- 因为其会生成不必要的副本(有很多拷贝)
- 由于大部分 STL 算法在计算时都会产生拷贝,因此不适合高效编程
- 但是在 C++ 20 部分算法会有所改善(使用移动语义)
- 使用 range 进行组合可以改善算法在执行时产生不必要副本的问题
总结
关键 API
- std::accumulate()
- std::reduce()
- std::multiplies<>()
- std::find_if()
- std::partition()
- std::stable_partition()
- std::remove_if()
- std::remove()
- std::copy_if()
- std::transform()
- std::find_if()
- std::all_of()
- std::any_of()
第三章 函数对象
3.1 函数和函数对象
- 函数是一组命名语句的集合
- 可以被程序的其他部分调用,或在递归中被自己调用
- C++ 提供了几种不同的定义函数方式
- 类 C 语法
int max(int arg) {...}
- 末尾返回类型的格式
auto max(int arg) -> int {...}
- 类 C 语法
3.1.1 自动推断返回值类型
- C++14 开始,完全可以忽略返回值类型,而由编译器根据 return 语句中的表达式进行推断
int answer = 1; auto ask() { return answer; } const auto& ask() { return answer; }
- 还可以使用
decltype(auto)
作为返回值类型decltype(auto) ask() { return answer; }
- 如果想要完美地传递结果,可以使用
- 不加修改地把返回的结果直接返回
template <typename Object, typename Function> decltype(auto) call_on_object(Object&& object, Function func) {return func(std::forward<Object>(object)); }
- 转发引用 (forwading reference)
&&
- 允许接收常对象,也可以接收普通对象和临时值
std::forward
原样传递参数
3.1.2 函数指针
- 是一个存放函数地址的变量,可以通过这个变量调用该函数
- 函数指针(引用)都是函数对象
auto ask() -> int { return 1; }using function_ptr = decltype(ask)*;class convertible_to_function_ptr {public:operator function_ptr() const{return ask;} };auto ask_ptr = &ask; //函数指针 std::cout << ask_ptr() << std::endl; auto& ask_ref = ask; //函数引用 std::cout << ask_ref() << std::endl; convertible_to_function_ptr ask_weapper; //可以自动转换成函数指针的对象 std::cout << ask_weapper() << std::endl;
3.1.3 调用操作符重载
- 除了创建可以转换成函数指针的类型,C++ 还提供了一个更好的方式创建类似函数的类型
- 创建一个类并重载它们的调用操作符
- 调用操作符可以有任意数目的参数,参数可以是任意类型
- 因此可以创建任意签名的函数对象
class function_object {public:return_type operator()(arguments) const{...} };
- 函数对象的优点
- 每一个实例都有自己的状态,不论是可变状态还是不可变状态
- 这些状态可以用于自定义函数的行为,而无需调用者指定
//创建一个有内部状态的函数对象类 class older_than {public:older_than(int limit): m_limit(limit){}bool operator() (const person_t &person) const{return person.arg() > m_limit;} private:int m_limit; }; ... //这样可以创建不同状态的对象实例 older_than older_than_2(2); older_than_2(person);
3.1.4 创建通用函数对象
- 可以使用模板让函数对象更通用
class older_than {public:older_than(int limit): m_limit(limit){}template <typename T>bool operator() (T &&object) const{return std::forward<T>(object).age() > m_limit;} private:int m_limit; }; ... older_than predicate(1); //函数对象可以作为谓词传递给算法 std::count_if(persons.cbegin(), persons.cend(), predicate);
3.2 lambda 和闭包 (Closure)
- lambda
- 是创建匿名函数对象的语法糖
- 允许创建内联函数对象
- 在要使用它们的地方
- 而不是在编写函数之外
3.2.1 lambda 语法
- 由三个主要部分组成
- 头
- 参数列表
- 体
[a, &b](int x, int y) { return a*x + b*y; }
- 头
[a, &b]
指明了包含 lambda 的范围的哪些变量在体中可见- 变量可以作为值也可以作为引用进行捕获
- 如果值捕获,则lambda对象保存这个值的副本
- 如果引用进行捕获,则只保存原来值的引用
- 也可以不声明所需要捕获的变量,由编译器捕获 lambda 体中使用的变量
- 所有变量都作为值进行捕获,可以写作
[=]
- 所有变量都作为引用捕获,可以写作
[&]
- 以值的方式捕获 this 指针,可以写作
[this]
- 除了 a 以外都以值的方式捕获,可以写作
[=, &a]
- 除了 a 以外都以引用的方式捕获,可以写作
[&, a]
3.2.2 lambda 详解
- 示例
std::count_if(m_employees.cbegin(), m_employees.cend(),[this, &team_name](const person_t &employee){return team_name_for(employee) == team_name;});
- C++ 在编译时,lambda 表达式将转换成一个包含两个成员变量的新类
- 这个类包含一个与 lambda 有相同参数和体的调用操作符
- 求解 lambda 表达式时,除了要创建类以外,还要创建一个称作闭包的类实例
- 一个包含某些状态和执行上下文的类对象
- 注意:
- lambda 的调用操作符默认是 const 的
- 如果需要修改捕获变量的值,可以使用 mutable
- 但是应避免这种用法
- lambda 的调用操作符默认是 const 的
3.2.3 在 lambda 中创建任意成员变量
- 可以使用扩展语法
[session = std::move(sessiont), time = current_time()]
3.2.4 通用 lambda 表达式
- 通过指明参数类型为 auto 的方式,lambda 允许创建通用的函数对象
auto predicate = [limit = 42](auto&& object) {return object.age() > limit; }
- 通用 lambda 是一个调用操作符模板化的类,而不是一个包含调用操作符的模板类
- 在创建 lambda 时,如果有多个参数声明为 auto, 这些参数的类型都需要单独进行推断
- 如果通用 lambda 的所有参数同属一种类型,可以使用 decltype 声明后续参数类型
[] (auto first, decltype(first) second) {...}
- C++20 lambda 语法被扩展,允许显示声明模板参数,而不需要声明为 decltype
[] <typename T> (T first, T scend) {...}
3.3 编写比 lambda 更简洁的函数对象
- 可以提供一些方式,让用户编写谓词函数
- 使用一个类重载调用操作符
- 支持比较的谓词函数实现
class error_test_t {public:error_test_t(bool error = true) : error_{ error }{}template<typename T>auto operator()(T&& value) const -> bool{return error_ == (bool)std::forward<T>(value).error();}error_test_t operator==(bool test) const{//如果 test 为 true,就返回谓词当前的状态//如果为 false , 就返回状态的逆状态return error_test_t(test ? error_ : !error_);}error_test_t operator!() const {//返回当前谓词的逆状态return error_test_t(!error_);} private:bool error_; };error_test_t error(true); error_test_t not_error(false);ok_responses = filter(responses, not_error); ok_responses = filter(responses, !error); ok_responses = filter(responses, error == false);failed_responses = filter(responses, error); failed_responses = filter(responses, error == true); failed_responses = filter(responses, not_error == false);
3.3.1 STL 中的操作符函数对象
- 算术操作符
- std::plus
- std::minus
- std::multiplies
- std::divides
- std::modulus
- std::negates
- 比较操作符
- std::equal_to
- std::not_equal_to
- std::greater
- std::less
- std::greater_equal
- std::less_equal
- 逻辑操作符
- std::logical_and
- std::logical_or
- std::logical_not
- 位运算操作符
- std::bit_and
- std::bit_or
- std::bit_xor
- 菱形操作符
- C++14开始,在调用标准库中的操作符包装器时,无需指明类型
- 可以写作
std::greater<>()
, 而不用写作std::greater<int>()
- 在调用时会自动推断参数的类型
- 可以写作
- C++14开始,在调用标准库中的操作符包装器时,无需指明类型
3.4 用 std::function 包装函数对象
- 如果要接收函数对象作为参数,或创建变量保存 lambda 表达式,到目前为止只能依赖自动类型检测
- 标准库提供了 std::function 类模板,可以包装任何类型的函数对象
std::function<float(float, float)> test_function;
test_function = std::fmaxf; //普通函数
test_function = std::multiplies<float>(); //含有调用操作符的类
test_function = std::multiplies<>(); //包含通用调用操作符的类
test_function = [x](float a, float b) { return a*x + b; }; // lambda 表达式
test_function = [x](auto a, auto b) { return a*x + b; }; //通用 lambda
- std::function 并不是对包含的类型进行模板化,而是对函数对象的签名进行模板化
- 还可以存储不提供普通调用语法的内容,例如
- 类的成员变量和类的成员函数
- 本书把函数对象连同指向成员变量和函数的指针称为 callables
- 使用 std::function 注意事项
- 不能滥用,因为它有明显的性能问题
- 为了隐藏包含的类型并提供一个对所有可调用类型的通用接口,其使用类型擦除技术
- 它是基于虚成员函数调用,因为在运行时进行,编译器不能在线调用,失去优化机会
- 为了隐藏包含的类型并提供一个对所有可调用类型的通用接口,其使用类型擦除技术
- 虽然调用操作符限定为 const, 但它可以调用非 const 对象。
- 在多线程代码中,容易导致各种问题
- 不能滥用,因为它有明显的性能问题
- 小函数对象优化
- 当包装的对象是函数指针或者 std::reference_wrapper时,小对象优化就会执行
- 这些可调用对象就存储在 std::function 对象中,无须动态分配任何内存
- 优化的最大值
- 与编译器和标准库的实现有关
- 比较大的对象需要动态分配内存,通过指针访问对象 std::function
- 在调用 std::function 的调用操作符时,由于 std::function 对象的创建和析构,可能对性能产生影响。
总结
关键 API
- decltype(auto)
- std::forward()
闭关之 C++ 函数式编程笔记(一):函数式编程与函数对象相关推荐
- scala函数式编程笔记: 纯函数式状态
scala函数式编程:纯函数式状态读书笔记 Overview: 带状态的方法的声明式实现可能带有副作用,难以保持引用透明. 以纯函数式的方式实现带状态的函数的关键在于让状态更新是显式的,不要以副作用方 ...
- java并发编程笔记_java并发编程笔记(一)——并发编程简介
java并发编程笔记(一)--简介 线程不安全的类示例 public class CountExample1 { // 请求总数 public static int clientTotal = 500 ...
- 【C++】黑马程序员 | c++教程从0到1入门编程笔记 | c++提高编程
配套视频:https://www.bilibili.com/video/BV1et411b73Z 文章目录: 一.C++核心编程 二.C++提高编程 1 模板 本阶段主要针对C++泛型编程和STL技术 ...
- 网络编程笔记之网络编程入门
网络编程的概念 网络编程最主要的工作就是在发送端把信息通过规定好的协议进行组装包,在接收端按照规定好的协议把包进行解析,从而提取出对应的信息,达到通信的目的.中间最主要的就是数据包的组装,数据包的过滤 ...
- [PYTHON] 核心编程笔记(18.多线程编程)
18.1 引言/动机 18.2 线程和进程 18.2.1 什么是进程(重量级进程)? 计算机程序只不过是磁盘中可执行的,二进制(或其他类型)的数据,他们只有在被读取到内存中,被操作系统调用时才开始他们 ...
- windows编程笔记(win32编程)以及其在游戏开发中的作用
directX11 win32只是局限于windows平台,但是也是非常不错的api 1,windows系统的宏真的很多,但是这只是一种标识方式罢了 2,windows的消息循环机制: 我把这个称之为 ...
- 小苏的Shell编程笔记之六--Shell中的函数
http://xiaosu.blog.51cto.com/2914416/531247 Shell函数类似于Shell脚本,里面存放了一系列的指令,不过Shell的函数存在于内存,而不是硬盘文件,所以 ...
- 闭关之 C++ 函数式编程笔记(四):monad 和 模板元编程
目录 第十章 monad 注意 10.1 仿函数并不是以前的仿函数 10.1.1 处理可选值 10.2 monad: 更强大的仿函数 10.3 基本的例子 10.4 range 与 monad 的嵌套 ...
- 闭关之 C++ 函数式编程笔记(二):偏函数、组合、可变状态与惰性求值
目录 第四章 以旧函数创建新函数 4.1 偏函数应用 4.1.1 把二元函数转成一元函数的通用方法 4.1.2 使用 std::bind 绑定值到特定的函数参数 4.1.3 二元函数参数的反转 (这节 ...
最新文章
- Python反爬研究总结
- Leetcode 106. 从中序与后序遍历序列构造二叉树 解题思路及C++实现
- SSM框架-使用MyBatis Generator自动创建代码
- 通过改变环境来改变自己的方法:屡试不爽
- 周志华《机器学习》课后习题解析(第二章)模型评估与选择
- IOS设计模式之四(备忘录模式,命令模式)
- C++实现链式基数排序
- python自定义类型转换_Python JSONDecoder自定义null类型的转换
- 人脸识别门禁接线图_自制宿舍NFC门禁教程——当你老是忘带寝室钥匙,并且有一个闲置的充电宝的时候...
- 安装Oracle 11g RAC R2 之Linux DNS 配置
- 离职后前公司老大叫我回去帮忙,怎么委婉拒绝?
- hdu3987 Harry Potter and the Forbidden Forest 最小割边数
- ESP8266-NodeMCU驱动TFT-SPI彩屏(驱动芯片ILI9341)- 第一个例程【1】
- 每周一品 · 无线充电设备中的磁性材料
- OpenCV第五章练习p163_5~8
- java心跳监控服务_JavaHeartBeat-应用服务器心跳检测
- 调节e18-d80nk的测量距离_教程 | GOM数字图像处理三维光学测量系统(ARAMIS) 的设备标定方法...
- HTML Input标签输入限制
- 乌班图与win10作为文件服务器,win10与子系统ubuntu之间互访文件
- pptp连接服务器无响应,解决PPTP客户端拨号不成功