目录
上一章节:
一、引言
这里主要介绍一下当下C++编程开发中比较常见的一种范式——函数式编程, 它以函数为核心,强调不可变性、高阶函数等概念,为我们处理复杂逻辑提供了新的视角和方法。这里给大家做一个简单的入门,以及笔者自己接触到的函数式编程的方式方法。
一、函数式编程基础
- 不可变性:在函数式编程里,数据一旦创建就不可改变。比如在传统 C++ 中,我们可能会这样写代码:
int a = 5;
a = 10; // 修改变量a的值
而在函数式编程理念下,我们更倾向于通过函数调用来产生新的值,而不是修改原有变量。例如:
int add(int num) {
return num + 5;
}
int result = add(5); // result为10,没有修改传入的参数
- 高阶函数:是指接受函数作为参数,或者返回一个函数的函数。C++ 中的std::for_each 就是一个高阶函数的例子:
#include <iostream>
#include <algorithm>
#include <vector>
void print(int num)
{
std::cout << num << " ";
}
int main()
{
std::vector<int> vec = {1, 2, 3, 4, 5};
std::for_each(vec.begin(), vec.end(), print);
return 0;
}
这里std::for_each 接受了print 函数作为参数,对容器中的每个元素进行操作。
- 闭包:闭包是一个函数对象,它可以捕获其创建环境中的变量。在 C++ 中,Lambda 表达式就常用来创建闭包。例如:
#include <iostream>
#include <vector>
int main()
{
int factor = 2;
auto multiply = [factor](int num) {
return num * factor; };
std::vector<int> vec = {1, 2, 3};
for (int num : vec) {
std::cout << multiply(num) << " ";
}
return 0;
}
这里multiply 这个 Lambda 表达式捕获了外部的factor 变量,形成了闭包。
- 惰性求值:惰性求值是指表达式只有在真正需要结果时才进行计算。在 C++ 中,虽然没有像某些函数式编程语言那样原生支持惰性求值,但我们可以通过一些技巧来模拟。比如自定义一个延迟计算的类模板。
三、Lambda 表达式
Lambda 表达式是 C++ 函数式编程中非常重要的一部分。它允许我们在代码中快速定义匿名函数。
其本质是匿名函数,能够捕获一定范围的变量,与普通函数不同,可以在函数内部定义;
例如,要对一个整数数组进行排序,我们可以使用 Lambda 表达式来指定排序规则:
#include <iostream>
#include <algorithm>
#include <vector>
int main()
{
std::vector<int> vec = {5, 3, 1, 4, 2};
std::sort(vec.begin(), vec.end(), [](int a, int b) {
return a < b; });
for (int num : vec)
{
std::cout << num << " ";
}
return 0;
}
这里的 Lambda 表达式[](int a, int b) { return a < b; } 定义了升序排序的比较规则。
作用:
- 简化程序结构,因为优化了函数命名与函数传参;
- 提高程序运行效率,因为优化了函数调用、函数返回等消耗;
- 适用于简单功能的函数优化;
Lambda 表达式捕获值的方式:
捕获方式 | 说明 |
---|---|
= | 按值捕获,lambda内部可以使用,但是无法更改值 |
& | 按地址捕获,lambda内部可以使用,同时也更改了实际值 |
变量名 | 按值捕获,可用不可改 |
&变量名
|
引用捕获,可用可改 |
副本捕获 | c++14后可以自定义变量名 = 捕获变量,但是无法通过副本名改变变量名 |
#include <iostream>
int main(int argc, char **argv)
{
int num1 = 5;
int num2 = 6;
auto func_add = [&num1,num2]() //num1可修改,num2不可修改
{
num1 = 7;
return num1 + num2;
};
auto func_add1 = [=](int a, int b)
{
a = 1; //这里修改的只是形参a/b的值,不会改变num1与num2的值
b = 1;
return a + b;
};
auto func_add2 = [&]
{
// num1 = 1; //按引用传递,可用可修改num1与num2的值
return num1 + num2;
};
std::cout<<func_add()<<std::endl;
std::cout<<"num1 = "<<num1<<" num2 = "<<num2<<std::endl;
std::cout<<func_add1(num1,num2)<<endl;
std::cout<<"num1 = "<<num1<<" num2 = "<<num2<<std::endl;
std::cout<<func_add2()<<endl;
std::cout<<"num1 = "<<num1<<" num2 = "<<num2<<std::endl;
return 0;
}
注意:
lambda 不可以包含static修饰的变量及全局变量;且避免复杂化 ;
四、函数对象
- 函数对象的定义和使用:函数对象是一个类或结构体,它重载了函数调用运算符"()"。例如:
#include <iostream>
#include <string>
#include <functional>
using namespace std;
template <typename T>
class Add
{
public:
Add() = default;
void operator()(T &&a, T &&b) //重载函数运算符,采用的是万能引用
{
cout<<a+b<<endl;
}
};
int main(int argc ,char **argv)
{
Add<int> c_add;
c_add.operator()(5,6); //利用成员函数的形式调用
c_add(5,6); //采用函数成员方式
plus<int> p1;
cout<<p1(5,6)<<endl; //使用系统函数对象库
return 0;
}
这里Adder 类就是一个函数对象, 通过重载() 运算符,使得它的对象可以像函数一样被调用。
- STL 中的函数对象:C++ STL 中提供了很多预定义的函数对象,如std::plus、std::less 等。例如使用std::plus 来对两个数求和:
#include <iostream>
#include <functional>
int main()
{
std::plus<int> plus_op;
int result = plus_op(5, 3);
std::cout << result << std::endl;
return 0;
}
C++ STL 提供了丰富的算法,这些算法很多都体现了函数式编程的思想。比如std::accumulate 可以用来对容器中的元素进行累加:
#include <iostream>
#include <numeric>
#include <vector>
int main() {
std::vector<int> vec = {1, 2, 3, 4, 5};
int sum = std::accumulate(vec.begin(), vec.end(), 0);
std::cout << sum << std::endl;
return 0;
}
函数对象与普通函数对比:
- 函数对象比一般函数更灵活,因为它可以拥有状态(state),事实上,对于相同的函数对象可以设置两个状态不同的实例;普通函数没有状态;
- 每个函数对象都有其类型,因为你可以将函数对象的类型当做template参数传递,从而指定某种行为;
- 执行速度上,函数对象通常比函数指针更快;
五、函数适配器
在编程中用于封装和管理函数或可调用对象(如函数指针、函数对象、Lambda 表达式等 ),让函数使用更灵活通用。以 C++ 为例,其标准库中的 std::function 是常用的函数适配器,本质是类模板 。它能 存储、复制和调用任何可调用对象,为不同可调用对象提供统一调用接口,使用者无需关心其具体类型。std::function ,表明可适配接收两个int 参数并返回int 类型值的可调用对象 。
实例:
1、适配普通函数
#include <iostream>
#include <functional>
int add(int a, int b) {
return a + b;
}
int main() {
std::function<int(int, int)> func = add;
std::cout << func(3, 4) << std::endl;
return 0;
}
2、适配 Lambda 表达式
#include <iostream>
#include <functional>
int main() {
std::function<int(int, int)> func = [](int a, int b) {
return a * b;
};
std::cout << func(3, 4) << std::endl;
return 0;
}
3、适配函数对象(仿函数)
#include <iostream>
#include <functional>
struct Subtract {
int operator()(int a, int b) const {
return a - b;
}
};
int main() {
std::function<int(int, int)> func = Subtract();
std::cout << func(5, 3) << std::endl;
return 0;
}
Subtract 结构体定义了函数调用运算符() ,是函数对象 。std::function 将其包装后,可通过func 调用实现减法。
使用场景
- 泛型编程:模板函数中,可将不同类型可调用对象(函数指针、Lambda、函数对象等)包装后作为参数传递,使模板函数能处理多种调用逻辑,增强代码通用性与灵活性。例如编写通用算法模板,可接收不同比较规则的函数包装器实现自定义排序等操作。
- 回调函数 :在事件驱动编程(如图形界面开发、网络编程 )中,常需设置回调函数。用函数包装器可方便存储和管理这些回调,在特定事件发生时调用。如注册按钮点击事件回调,可将处理逻辑写成普通函数、Lambda 等,再用函数包装器管理并传递给按钮组件。
- 异步编程 :多线程或异步任务场景下,函数包装器可存储要在新线程或异步环境执行的函数。如使用std::thread创建线程时,可将函数包装器作为线程执行任务,方便管理任务逻辑。
- 日志记录与性能监控 :通过包装器,可在函数执行前后添加日志记录代码,记录输入参数、执行时间等信息,辅助调试和性能优化;也能进行性能分析,记录函数执行耗时、资源占用等指标。
- 权限验证与异常处理 :在函数执行前,利用包装器进行权限验证,确保只有有权限用户能调用;执行过程中捕获异常并处理,如打印错误信息、进行重试等操作 ,增强程序稳定性与安全性。
六、bind函数适配器
(1)、主要用在 函数已经存在,但是现有参数较多,减少实际所需参数个数的一种方法;
(2)、本质, bind也是一个函数模板,返回值是一个仿函数 ,是可调用对象;
(3)、bind可以绑定的对象:①普通函数;②lambda表达式;③函数对象;④类的成员函数;⑤类的数据成员;
#include <iostream>
#include <functional>
using namespace std;
template <typename T>
class Add
{
public:
T operator()(T a, T b, T c)
{
print();
return a + b;
}
void operator()(const T &a)
{
cout << a << endl;
}
void print()
{
cout << "function add!" << endl;
}
int m_result;
};
int add(int a, int b, int c)
{
cout << "a = " << a << " b = " << b << endl;
return a + b + c;
}
int main()
{
//普通函数
function<int(int,int)> my_add = std::bind(add,std::placeholders::_1,std::placeholders::_2,0);
cout << my_add(5,6) << endl;
function<int()> my_add2 = std::bind(add,7,8,0);
cout << my_add2() << endl;
//lambda表达式
auto lambda_func = [=](int a, int b, int c)
{
return a + b + c;
};
function<int(int,int)> my_add3 = std::bind(lambda_func,std::placeholders::_2,std::placeholders::_1,0);
cout << my_add3(3,4) << endl;
function<int()> my_add4 = std::bind(lambda_func,3,4,0);
cout << my_add4() << endl;
//函数对象
Add<int> c_add;
function<int(int,int)> my_add5 = std::bind(c_add,std::placeholders::_2,std::placeholders::_1,0);
cout << my_add5(4,5) << endl;
function<int()> my_add6 = std::bind(c_add,5,6,0);
cout << my_add6() << endl;
return 0;
}
七、函数式编程的应用
- 数据处理:在处理大量数据时,函数式编程可以让代码更简洁和易于理解。比如对一个包含学生成绩的数组进行筛选,找出成绩大于 80 分的学生,使用函数式编程风格的代码可能如下:
#include <iostream>
#include <vector>
#include <algorithm>
struct Student {
std::string name;
int score;
};
int main()
{
std::vector<Student> students = {{"Alice", 85}, {"Bob", 70}, {"Charlie", 90}};
std::vector<Student> high_scores;
std::copy_if(students.begin(), students.end(), std::back_inserter(high_scores), [](const Student& s) {
return s.score > 80; });
for (const auto& student : high_scores)
{
std::cout << student.name << " : " << student.score << std::endl;
}
return 0;
}
- 并发编程:函数式编程的不可变性等特性在并发编程中很有优势,因为不可变的数据不用担心多线程访问时的竞争问题。例如,在使用std::async 进行异步任务时,可以传递函数式风格的函数对象。
- 机器学习:在机器学习领域,函数式编程可以用于数据预处理、模型训练过程中的函数组合等场景。比如对数据集进行一系列的变换操作,可以通过组合不同的函数来实现。
八、总结
C++ 函数式编程为我们提供了一种强大且优雅的编程方式,无论是处理简单逻辑还是复杂的应用场景,都能展现出其独特的魅力。通过深入理解和应用这些概念,我们可以编写出更高效、更易维护的代码。