深入浅出闭包

news2024/12/29 10:17:49

目录

一、闭包概念

二、重载operator()

三、lambda表达式

3.1 lambda表达式介绍

3.2 lambda表达式语法

3.2.1 [capture-list]捕捉列表

3.2.2(parameters):参数列表

3.2.3 mutable关键字

3.2.4 ->returntype:返回值类型

3.2.5 {statement}:函数体

3.2.6 注意事项

3.3 lambda表达式实现原理

四、包装器

4.1 function

4.1.1 使用场景

 4.1.2 包装类的成员函数

4.2 bind

4.2.1 调整参数个数

4.2.2 调整参数顺序


一、闭包概念

闭包有很多种定义。其中一种说法是,闭包是带有上下文的函数(即有状态的函数)。也有人认为闭包是能够读取其他函数内部变量的函数。

闭包有属于自己的变量, 这些个变量的值是创建闭包的时候设置的, 并在调用闭包的时候, 可以访问这些变量。

函数是代码, 状态是一组变量,将代码和一组变量捆绑,就形成了闭包,内部包含static变量的函数不是闭包,因为static变量不能捆绑。你不能捆绑不同的 static 变量,这个在编译时已经确定了。

闭包的状态捆绑, 必须发生在运行时。

在C++中,闭包有三种实现方式:

1. 重载operator()函数 (即实现仿函数)

2. lambda表达式

3. 包装器(即function和bind)

二、重载operator()

因为闭包是一个函数+状态, 这个状态通过隐含的 this 指针传入,所以闭包必然是一个函数对象。因为成员变量就是极好的用于保存状态的工具,因此实现 operator() 运算符重载,该类的对象就能作为闭包使用。默认传入的 this 指针提供了访问成员变量的途径。

#include <iostream>
#include <vector>
#include <string>
#include <algorithm>
using namespace std;
struct Goods{
    Goods(const char* str, double price, int evaluate)
        :_name(str)
        , _price(price)
        , _evaluate(evaluate){}

    string _name;
    double _price;
    int _evaluate; //评价
};
struct ComparePriceLess{
    bool operator()(const Goods& gl, const Goods& gr){
        return gl._price < gr._price;
    }
};
struct ComparePriceGreater{
    bool operator()(const Goods& gl, const Goods& gr){
        return gl._price > gr._price;
    }
};
int main()
{
    vector<Goods> v = { { "苹果", 2.1, 5 }, { "香蕉", 3, 4 }, { "橙子", 2.2,3 }, { "菠萝", 1.5, 4 } };
    sort(v.begin(), v.end(), ComparePriceLess());
    for (auto& e : v) {
        cout << e._name << "\t" << e._price << "\t" << e._evaluate << endl;
    }
    cout << endl;
    sort(v.begin(), v.end(), ComparePriceGreater());
    for (auto& e : v) {
        cout << e._name << "\t" << e._price << "\t" << e._evaluate << endl;
    }
}

上面的写法太复杂了,每次为了实现一段闭包代码,都要重新去写一个函数对象类,特别是这些类的命名,这些都给编程者带来了极大的不便。因此,在C++11语法中出现了lambda表达式

三、lambda表达式

3.1 lambda表达式介绍

lambda表达式是一个匿名对象,用于创建匿名的函数对象,以简化编程工作。当lambda表达式有捕获变量时(即这个对象有自己的数据)就形成了闭包。lambda表达式的类型在C++11中被称为"闭包类型",每一个lambda表达式会产生一个临时对象(右值)。

利用lambda表达式即可解决上述问题。

int main()
{
    vector<Goods> v = { { "苹果", 2.1, 5 }, { "香蕉", 3, 4 }, { "橙子", 2.2, 3 }, { "菠萝", 1.5, 4 } };

    sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2){
        return g1._price < g2._price; 
    });
    sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2){
        return g1._price > g2._price; 
    });
    sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2){
        return g1._evaluate < g2._evaluate; 
    });
    sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2){
        return g1._evaluate > g2._evaluate; 
    });
}

3.2 lambda表达式语法

[capture-list] (parameters) mutable -> return-type { statement }

3.2.1 [capture-list]捕捉列表

捕捉列表,该列表总是出现在lambda函数的开始位置,编译器根据[]来判断接下来的代码是否为lambda函数,捕捉列表能够捕捉上下文中的变量供lambda函数使用。捕捉列表描述了上下文中那些数据可以被lambda使用,以及是传值还是传引用。

3.2.2(parameters):参数列表

与普通函数的参数列表一致,如果不需要参数传递,则可以连同()一起省略。

注意:参数列表中不能有默认参数,不支持可变参数,所有参数必须有参数名。

3.2.3 mutable关键字

在lambda表达式中,若以传值方式捕获外部变量,则函数体中不能修改该外部变量,否则会引发编译错误。使用mutable关键字用以说明表达式体内的代码可以修改值捕获的变量。使用该修饰符时,参数列表不可省略(即使参数为空)。

#include<iostream>
using namespace std;
int main()
{
    int a = 0;
    //auto f1 = [=] { return a++; };             // error,修改按值捕获的外部变量
    auto f2 = [=]() mutable { 
        ++a;
        cout << "a = " << a << endl;//值传递
    };
    f2();//1
    cout << "a = " << a << endl;//0
    return 0;
}

3.2.4 ->returntype:返回值类型

用追踪返回类型形式声明函数的返回值类型,没有返回值时此部分可省略。返回值类型明确情况下,也可省略,由编译器对返回类型进行推导。

3.2.5 {statement}:函数体

在函数体内,除了可以使用其参数外,还可以使用所有捕获到的变量。

3.2.6 注意事项

1. 父作用域指包含lambda函数的语句块

2. 语法上捕捉列表可由多个捕捉项组成,并以逗号分割

        比如:[=, &a, &b]:以引用传递的方式捕捉变量a和b,值传递方式捕捉其他所有变量
                   [&,a, this]:值传递方式捕捉变量a和this,引用方式捕捉其他变量

3. 捕捉列表不允许变量重复传递,否则就会导致编译错误

        比如:[=, a]:=已经以值传递方式捕捉了所有变量,捕捉a重复

4. 在块作用域以外的lambda函数捕捉列表必须为空

5. 在块作用域中的lambda函数仅能捕捉父作用域中局部变量,捕捉任何非此作用域或者非局部变量都会导致编译报错。(静态变量和全局变量都不可捕捉)

6. lambda表达式之间不能相互赋值,即使看起来类型相同。

7. lambda表达式可以向函数指针转换。前提是lambda函数并没有捕获任何变量,且函数指针所示的函数原型必须跟lambda有着相同的调用方式。

3.3 lambda表达式实现原理

从使用方式上来看,仿函数与lambda表达式完全一样。函数对象将rate作为其成员变量,在定义对象时给出初始值即可。lambda表达式通过捕获列表可以直接将该变量捕获到。

#include <iostream>
using namespace std;
class Rate
{
public:
    Rate(double rate) : _rate(rate) {}
    double operator()(double money, int year){
        return money * _rate * year;
    }
private:
    double _rate;
};
int main()
{
    // 函数对象
    double rate = 0.49;
    Rate r1(rate);
    r1(10000, 2);
    // lambda
    auto r2 = [=](double monty, int year)->double {
        return monty * rate * year;
    };
    r2(10000, 2);
    return 0;
}

实际在底层编译器对于lambda表达式的处理方式,完全就是按照函数对象的方式处理的。即:若定义了一个lambda表达式,编译器底层会生成一个lambda_uuid类,该类中重载了operator()。所以说"匿名函数"仅仅是对上层程序员而言的,底层仍有一定的命名规则。

所以为什么lambda表达式之间不能相互赋值?
因为各个lambda表达式属于不同的类(即使看起来类型相同,但其uuid码不一致)

四、包装器

4.1 function

function包装器,也叫作适配器。C++中的function本质是一个类模板。

C++11中新增了std::function类模板,它是对C++中现有的可调用的一种类型安全的包裹,通过指定它的模板参数,用统一的方式处理lambda、函数对象、函数指针等,并允许保存和延迟执行它们

4.1.1 使用场景

#include <iostream>
using namespace std;
template<class F, class T>
T useF(F f, T x)
{
    static int count = 0;
    cout << "count:" << ++count << endl;
    cout << "count:" << &count << endl;
    return f(x,x);
}
int f(int a, int b)
{
    return a + b;
}
struct Functor
{
public:
    int operator() (int a, int b){
        return a + b;
    }
};
int main()
{
    cout << useF(f, 11.11) << endl;//函数指针
    cout << useF(Functor(), 11.11) << endl;//函数对象

    //各个lambda表达式类型不同(lambda_uuid)
    cout << useF([](int a, int b)->int { return a + b; }, 11.11) << endl;
    cout << useF([](int a, int b)->int { return a + b; }, 11.11) << endl;
    cout << useF([](int a, int b)->int { return a + b; }, 11.11) << endl;
    cout << useF([](int a, int b)->int { return a + b; }, 11.11) << endl;
    cout << useF([](int a, int b)->int { return a + b; }, 11.11) << endl;
    return 0;
}

用function即可解决上述问题

#include <iostream>
#include <functional>
using namespace std;
template<class F, class T>
T useF(F f, T x)
{
    static int count = 0;
    cout << "count:" << ++count << endl;
    cout << "count:" << &count << endl;
    return f(x,x);
}
int f(int a, int b)
{
    return a + b;
}
struct Functor
{
public:
    int operator() (int a, int b){
        return a + b;
    }
};
int main()
{
    std::function<int(int, int)> func1 = f;
    cout << useF(func1,11.11) << endl;
    std::function<int(int, int)> func2 = Functor();
    cout << useF(func2, 11.11) << endl;
    std::function<int(int, int)> func3 = [](int a, int b)->int { return a + b; };
    cout << useF(func3, 11.11) << endl;
    std::function<int(int, int)> func4 = [](int a, int b)->int { return a + b; };
    cout << useF(func4, 11.11) << endl;
    return 0;
}

 4.1.2 包装类的成员函数

#include <iostream>
#include <functional>
using namespace std;
class Plus
{
public:
    static int plusi(int a, int b){
        return a + b;
    }
    double plusd(double a, double b){
        return a + b;
    }
};
int main()
{
    std::function<int(int, int)> func4 = &Plus::plusi;//静态成员函数
    cout << func4(1, 2) << endl;
    std::function<double(Plus, double, double)> func5 = &Plus::plusd;//普通成员函数
    cout << func5(Plus(), 1.1, 2.2) << endl;
    return 0;
}

4.2 bind

bind通过把外部变量函数绑定在一起实现闭包。

std::bind函数定义在<functional>头文件中,是一个函数模板。它就像一个函数包装器(适配器),接受一个可调用对象(callable object),生成一个新的可调用对象来“适应”原对象的参数列表。一般而言,我们用它可以把一个原本接收N个参数的函数,通过绑定一些参数,返回一个接收M个参数的新函数(即调整参数个数)。同时,使用std::bind函数还可以实现调整参数顺序等操作。

 

4.2.1 调整参数个数

#include <iostream>
#include <map>
using namespace std;

int Plus(int a, int b){
	return a + b;
}

int Mul(int a, int b){
	return a * b;
}

class Sub
{
public:
	int sub(int a, int b){
		return a - b;
	}
};

int main()
{
    //无法用同种function包装器接收
	function<int(int, int)> funcPlus = Plus;
	function<int(Sub, int, int)> funcSub = &Sub::sub;
	function<int(int, int)> funcMul = Mul;

    
    //通过bind调整Sub参数个数
	map<string, function<int(int, int)>> opFuncMap =
	{
		{ "+", Plus},
		{ "-", bind(&Sub::sub, Sub(), placeholders::_1, placeholders::_2)},
		{ "*", Mul}
	};

	cout << opFuncMap["+"](1, 2) << endl;
	cout << opFuncMap["-"](1, 2) << endl;
	cout << opFuncMap["*"](1, 2) << endl;

	return 0;
}

4.2.2 调整参数顺序

调整参数顺序这个功能的使用场景较少。

#include <iostream>
using namespace std;

int Div(int a, int b){
	return a / b;
}

int main()
{
	int x = 2, y = 10;
	cout << Div(x, y) << endl;
	function<int(int, int)> bindFunc = bind(Div, placeholders::_2, placeholders::_1);
	cout << bindFunc(x, y) << endl;

	return 0;
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/132403.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

基于OpenSSL的安全Web Server实现

目录 一、实验目的 二、实验软硬件要求 三、实验预习 四、实验内容&#xff08;实验步骤、测试数据等&#xff09; 实验步骤 1编辑代码 2解决报错 3准备网页 五、实验体会&#xff08;遇到的问题及解决方法&#xff09; 六、服务器代码 七、测试网页代码 一、实验目…

(02)Cartographer源码无死角解析-(43) 2D栅格地图→

讲解关于slam一系列文章汇总链接:史上最全slam从零开始&#xff0c;针对于本栏目讲解(02)Cartographer源码无死角解析-链接如下: (02)Cartographer源码无死角解析- (00)目录_最新无死角讲解&#xff1a;https://blog.csdn.net/weixin_43013761/article/details/127350885 文末…

利用Python构建Wiki中文语料词向量模型

利用Python构建Wiki中文语料词向量模型试验 完整代码下载地址&#xff1a;利用Python构建Wiki中文语料词向量模型 本实例主要介绍的是选取wiki中文语料&#xff0c;并使用python完成Word2vec模型构建的实践过程&#xff0c;不包含原理部分&#xff0c;旨在一步一步的了解自然语…

CH450/TM1637 驱动调试

CH450&#xff1a;支持带有中断的扫描键盘、数码显示&#xff1b; TM1637&#xff1a;键盘扫描/数码显示&#xff1b; CH450/TM1637 I2C的时序调试有问题&#xff0c;总体上注意以下几点 MSB还是LSB&#xff0c;这两个不一样&#xff1b;&#xff08;TM1637从低到高传输&…

PhpStorm 使用全局搜索得不到结果

一、前言二、解决一、前言 全文搜索快捷键&#xff1a;ctrl shift f&#xff0c;如果 没有弹出搜索框。看下快捷键是不是被其他软件占用了&#xff1a;比如搜狗输入法现在的问题是&#xff1a;输入想要搜索的关键字&#xff0c;但是没用搜出来结果&#xff08;实际上关键字在…

BurpSuite与Xray联动进行被动扫描实战

今天继续给大家介绍渗透测试相关知识&#xff0c;本文主要内容是BurpSuite与Xray联动进行被动扫描实战。 免责声明&#xff1a; 本文所介绍的内容仅做学习交流使用&#xff0c;严禁利用文中技术进行非法行为&#xff0c;否则造成一切严重后果自负&#xff01; 再次强调&#xf…

Linux权限及其理解

文章目录&#xff1a;Linux权限的概念Linux权限管理文件访问者的分类&#xff08;人&#xff09;文件类型和访问权限&#xff08;事物属性&#xff09;文件权限值的表示方法文件访问权限的设置方法权限掩码目录的权限粘滞位总结Linux权限的概念 与其它系统相比&#xff0c;Lin…

文件包含漏洞渗透与攻防(一)

目录 前言 什么是文件包含漏洞 文件包含漏洞类型 本地文件包含 远程文件包含 PHP相关函数和伪协议 函数 PHP伪协议 CTF题目案例 文件包含漏洞挖掘与利用 URL关键字 代码审计 利用流程 文件包含漏洞修复方案 前言 随着网站业务的需求&#xff0c;程序开发人员一…

【ACWING】【图的广度遍历】【848有向图的拓扑顺序】

给定一个 n个点 m条边的有向图&#xff0c;点的编号是 1到 n&#xff0c;图中可能存在重边和自环。 请输出任意一个该有向图的拓扑序列&#xff0c;如果拓扑序列不存在&#xff0c;则输出 −1。 若一个由图中所有点构成的序列 A满足&#xff1a;对于图中的每条边 (x,y)&#…

STM32MP157驱动开发——Linux CAN驱动

STM32MP157驱动开发——Linux CAN驱动一、简介1.电气属性2.CAN协议3.CAN速率4.CAN FD 简介二、驱动开发1.修改设备树2.FDCAN1控制器节点3.修复 m_can_platform.c4.使能 CAN 总线5.使能FDCAN外设驱动三、运行测试1.移植 iproute2 和 can-utils 工具2.测试1&#xff09;收发测试&…

数据结构排序

文章目录直接插入排序直接插入排序 核心代码 void InsertSort(int arr[], int n) {for (int i 2; i < n; i) ///直接从第二个元素开始遍历{if (arr[i - 1] > arr[i]) //判断前一个元素和当前元素的大小&#xff0c;若前一个元素小于当前元素才需要插入{arr[0] …

Markdown之折叠语法以及表格内父子折叠

背景 在编写接口文档的时候发现一些特别扭的问题&#xff0c;就是一个表格来说明入参和出参的时候&#xff0c;怎么去表达嵌套的父子关系呢&#xff1f;查看了大厂的接口文档&#xff0c;比如微信支付&#xff0c;他们是有完善的接口文档页面&#xff0c;也都全部标记出了表格…

pycharm-qt5-基础篇1

pycharm-qt5-基础篇1一: QT5介绍1> 主要的特性2> pycharm 外部工具及功能1. Qt Designer2. PyUic3> PyUrcc二: pycharm QT5 环境搭建1> 虚拟环境搭建2> 安装 pyqt5、pyqt5-tools3> 将QT工具添加到环境变量4> 配置PyCharm三: QT5 demo四: pyinstaller 打包…

Java 并发编程知识总结【三】

4. CompletableFuture 4.1 Future 和 Callable 接口 Future 接口定义了操作异步任务执行一些方法&#xff0c;如获取异步任务的执行结果、取消任务的执行、判断任务是否被取消、判断任务执行是否完毕等。 Callable 接口中定义了需要有返回的任务需要实现的方法。 使用途径&am…

详解Spring面试AOP

文章目录什么是 AOP&#xff1f;AOP作用AOP核心概念&#xff08;来自黑马程序课程&#xff09;AOP 解决了什么问题&#xff1f;AOP 为什么叫做切面编程&#xff1f;总结1 AOP的核心概念2 切入点表达式3 五种通知类型4 通知中获取参数AOP是面向切面编程&#xff0c;是一个设计思…

Java同学录系统同学录网站

简介 用户注册可以创建班级&#xff08;创建者即为群主&#xff09;&#xff0c;用户也可以查找班级申请加入&#xff0c;群主添加同学的联系方式等&#xff0c;可以在班级里留言&#xff0c;管理相册等&#xff0c;还可以指定其他人为群主或者解散班级群&#xff0c;群里的用…

【ROS】—— ROS快速上手(一)

文章目录前言1. ROS-melodic 安装2. ROS基本操作2.1 创建工作空间2.2 创建功能包2.3 HelloWorld(C版)2.4 HelloWorld(Python版)3. Vscode ROS 插件4. vscode 使用基本配置4.1 启动 vscode4.2 vscode 中编译 ros5. launch文件演示6. ROS文件系统7. ROS文件系统相关命令前言 &…

EMNLP22评测矩阵:FineD-Eval: Fine-grained Automatic Dialogue-Level Evaluation

总结 在选择维度时&#xff0c;有点意思。 FineD-Eval: Fine-grained Automatic Dialogue-Level Evaluation 一般对话生成任务的评测也是从多个维度出发&#xff0c;这篇文章先选择了几个相关性程度低的维度&#xff0c;然后&#xff0c;在挑选后的维度上&#xff0c;测评相…

动态规划经典题:编辑距离(hard) 详解,看了还不会你来砍我

&#x1f9f8;&#x1f9f8;&#x1f9f8;各位大佬大家好&#xff0c;我是猪皮兄弟&#x1f9f8;&#x1f9f8;&#x1f9f8; 文章目录一、最长公共子序列二、两个字符串的删除操作三、编辑距离Hard为了更好的理解&#xff0c;我们从易到难的来解决编辑距离的问题一、最长公共…

Tic-Tac-Toe可能棋局遍历的实现(python)

目录 1. 前言 2. 算法流程 3. 代码实现 4. 一个思考题&#xff1a;代码实现中的一个坑 5. 结果正确吗&#xff1f; 1. 前言 在上一篇博客中&#xff1a;Tic-Tac-Toe可能棋局搜索的实现&#xff08;python&#xff09;_笨牛慢耕的博客-CSDN博客Tic-Tac-Toe中文常译作井字棋…