【C++】C++11 lambda表达式

👀樊梓慕:个人主页

 🎥个人专栏:《C语言》《数据结构》《蓝桥杯试题》《LeetCode刷题笔记》《实训项目》《C++》《Linux》《算法》

🌝每一个不曾起舞的日子,都是对生命的辜负


目录

前言

C++11引入『 lambda表达式』的原因

lambda表达式的语法

如何调用lambda表达式 

捕捉列表

lambda表达式实现swap函数的不同方式

参数传引用

捕捉列表

 lambda表达式的底层是仿函数


前言

lambda表达式的引入是为了简化代码,提高代码的可读性,在某种角度上来看,lambda表达式实际上是一个匿名函数。


欢迎大家📂收藏📂以便未来做题时可以快速找到思路,巧妙的方法可以事半功倍。 

=========================================================================

GITEE相关代码:🌟樊飞 (fanfei_c) - Gitee.com🌟

=========================================================================


C++11引入『 lambda表达式』的原因

在现实案例中,排序往往是复杂类型的排序,比如网购商品,某种商品具有很多种属性,用户可以选择不同的排序策略,比如按照价格、口碑等等。

那么按照以前我们学习过的知识,我们可以实现不同的『 仿函数』来达到根据不同属性排序的目的。

但是这样会引发一个问题:代码可读性差。

比如某个程序员他非常professional,他可能会这样命名仿函数:

struct Goods
{
	string _name;  // 名字
	double _price; // 价格
	int _evaluate; // 评价

	Goods(const char* str, double price, int evaluate)
		:_name(str)
		, _price(price)
		, _evaluate(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());
	sort(v.begin(), v.end(), ComparePriceGreater());
}

我们可以非常清晰的明白两个仿函数的意义:

  • ComparePriceLess:按价格升序;
  • ComparePriceGreater:按价格降序;

但如果这个程序员很不友好,他有可能会这样命名仿函数:

  • Compare1;
  • Compare2;

此时你不能通过名字直接了解该仿函数的逻辑,就只能查找源码,当项目比较复杂时,很明显你不希望有这样的工作。

所以lambda表达式诞生了。

虽然你还没有学习lambda表达式,但是以下的代码逻辑你一定能懂:

int main()
{
	vector<Goods> v = { { "苹果", 2.1, 300 }, { "香蕉", 3.3, 100 }, { "橙子", 2.2, 1000 }, { "菠萝", 1.5, 1 } };

    //按价格升序排序
	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._num < g2._num;
	}); 

    //按数量降序排序
	sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2)
	{
		return g1._num > g2._num;
	}); 

	return 0;
}

 也就是说,我们可以清晰地直接看到排序的逻辑,这样在你阅读代码时就不需要查找定义了。

是不是有点像匿名对象的味道,lambda表达式可以看作是一种匿名函数。


lambda表达式的语法

lambda表达式书写格式:[capture-list](parameters)mutable->return-type{statement}

  • [capture-list]:捕捉列表。该列表总是出现在lambda函数的开始位置,编译器根据[]来判断接下来的代码是否为lambda函数,捕捉列表能够捕捉上下文中的变量供lambda函数使用。
  • (parameters):参数列表。与普通函数的参数列表一致,如果不需要参数传递,则可以连同()一起省略。
  • mutable:默认情况下,lambda函数总是一个const函数,mutable可以取消其常量性。使用该修饰符时,参数列表不可省略(即使参数为空)。(博主主观上感觉这个关键字没啥用)
  • ->return-type:返回值类型。用追踪返回类型形式声明函数的返回值类型,没有返回值时此部分可以省略。返回值类型明确情况下,也可省略,由编译器对返回类型进行推导,所以这块我们一般不写。
  • {statement}:函数体。在该函数体内,除了可以使用其参数外,还可以使用所有捕获到的变量。

lambda函数的参数列表和返回值类型都是可选部分,但捕捉列表和函数体是不可省略的,因此简单的lambda函数如下:

int main()
{
	[]{}; //最简单的lambda表达式
    []{ cout << "hello world" << endl; };
	return 0;
}

如何调用lambda表达式 

lambda表达式实际上可以理解为无名函数,该函数无法直接调用,如果想要直接调用,可借助auto将其赋值给一个变量:

int main()
{
	auto add = [](int a, int b) {return a + b; };
	cout << add(1, 2) << endl;
    return 0;
}

捕捉列表

捕捉列表描述了上下文中哪些数据可以被lambda函数使用,以及使用的方式是传值还是传引用。

其实捕捉列表有点类似于函数传参,捕捉过来的变量是拷贝的临时对象不可修改。 

  • [var]:表示值传递方式捕捉变量var。
  • [=]:表示值传递方式捕获所有父作用域中的变量(成员函数包括this指针)。
  • [&var]:表示引用传递捕捉变量var。
  • [&]:表示引用传递捕捉所有父作用域中的变量(成员函数包括this指针)。
  • [this]:表示值传递方式捕捉当前的this指针。

注意:

  • 父作用域指的是包含lambda函数的语句块。
  • 语法上捕捉列表可由多个捕捉项组成,并以逗号分割。
    • 比如[=, &a, &b]:以引用传递的方式捕捉a和b,值传递的方式捕捉其他所有变量。
    • 比如[&, a, this]:以值传递方式捕捉变量a和this,引用方式捕捉其他变量。
  • 捕捉列表不允许变量重复传递,否则会导致编译错误。
    • 比如[=, a]重复传递了变量a(引用可以)。
  • 在块作用域以外的lambda函数捕捉列表必须为空。
  • 在块作用域中的lambda函数仅能捕捉父作用域中的局部变量,捕捉任何非此作用域或者非局部变量都会导致编译报错。
  • lambda表达式之间不能相互赋值,即使看起来类型相同(后面讲底层为什么)。

lambda表达式实现swap函数的不同方式

参数传引用

int main()
{
	int a = 10, b = 20;
	auto Swap = [](int& x, int& y)
	{
		int tmp = x;
		x = y;
		y = tmp;
	};
	Swap(a, b); //交换a和b
	return 0;
}

捕捉列表

//方式1
int main()
{
	int a = 10, b = 20;
	auto Swap = [&]
	{
		int tmp = a;
		a = b;
		b = tmp;
	};
	Swap(); //交换a和b
	return 0;
}
//方式2
int main()
{
	int a = 10, b = 20;
	auto Swap = [&a, &b]
	{
		int tmp = a;
		a = b;
		b = tmp;
	};
	Swap(); //交换a和b
	return 0;
}

注意:实际当我们以[&]或[=]的方式捕获变量时,编译器一般只会对lambda表达式中用到的变量进行捕获,这个具体看编译器的优化。

传值方式捕捉不可行:

如果以传值方式进行捕捉,那么首先编译不会通过,因为传值捕获到的变量默认是不可修改的,所以我们需要利用mutable,并且此时参数列表不可省略。比如:

int main()
{
	int a = 10, b = 20;
	auto Swap = [a, b]()mutable
	{
		int tmp = a;
		a = b;
		b = tmp;
	};
	Swap(); //交换a和b?
	return 0;
}

但由于这里是传值捕捉,lambda函数中对a和b的修改不会影响外面的a、b变量,与函数的传值传参是一个道理,因此这种方法无法完成两个数的交换。

所以博主主观认为:mutable基本没啥用,一般用不上。


 lambda表达式的底层是仿函数

其实这里非常类似范围for,在学习C++11新特性的范围for时,我们可能觉得他非常神奇,但实际上底层还是利用的迭代器。

那这里lambda表达式也看起来非常神奇,但实际上底层就是仿函数。

如果我们定义了一个lambda表达式,那么编译器就会自动生成一个类,在该类中重载了operator(),大家想这是不是就是仿函数的实现啊,只不过是编译器替我们干了。

我们来观察一下:

首先写一个普通的仿函数:

class Rate
{
public:
	Rate(double rate) : _rate(rate)
	{}
	double operator()(double money, int year)
	{
		return money * _rate * year;
	}
private:
	double _rate;
};

然后我们分别调用这个仿函数,以及一个相同功能的lambda表达式:

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函数体的实现就是这个仿函数的operator()的实现。
  • 在调用lambda表达式时,参数列表和捕获列表的参数,最终都传递给了仿函数的operator()。

我们发现lambda表达式构造出来的仿函数对象后面加了很长的一段字符串,这段字符串是『 UUID-通用唯一识别码(Universally Unique Identifier) 』,目的就是为了防止构造出重名对象。

所以你知道为什么说『 虽然lambda表达式看起来类型相同,但是之间不能相互赋值』了么?

因为他们本质上都不是同一个类型。

int main()
{
	int a = 10, b = 20;
	auto Swap1 = [](int& x, int& y)
	{
		int tmp = x;
		x = y;
		y = tmp;
	};
	auto Swap2 = [](int& x, int& y)
	{
		int tmp = x;
		x = y;
		y = tmp;
	};
	cout << typeid(Swap1).name() << endl; //class <lambda_730de80e8951d4f1039a1c0cd8e63481>
	cout << typeid(Swap2).name() << endl; //class <lambda_9a1101c5726f53e39147e39ad3b29cda>
	return 0;
}

可以看到,就算是两个一模一样的lambda表达式,它们的类型都是不同的。

如果我们想要进行赋值,可以参考下面的案例:

void (*PF)();
int main()
{
	auto f1 = [] {cout << "hello world" << endl; };
	auto f2 = [] {cout << "hello world" << endl; };

	//f1 = f2; // 编译失败--->类型不同

	// 允许使用一个lambda表达式拷贝构造一个新的副本
	auto f3(f2);
	f3();
	// 可以将lambda表达式赋值给相同类型的函数指针
	PF = f2;
	PF();
	return 0;
}

=========================================================================

如果你对该系列文章有兴趣的话,欢迎持续关注博主动态,博主会持续输出优质内容

🍎博主很需要大家的支持,你的支持是我创作的不竭动力🍎

🌟~ 点赞收藏+关注 ~🌟

=========================================================================

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

蓝桥杯简单STL

目录 vector vector定义 vector访问 常用函数 size() ​编辑 push_back(num) pop_back() clear 迭代器&#xff08;iterator) 迭代器定义 遍历数组示例 insert(it, element) erase(it) 标准模板库--STL&#xff0c;它包含了多种预定义的容器、算法和迭代器&…

【Go语言快速上手(一)】 初识Go语言

&#x1f493;博主CSDN主页:杭电码农-NEO&#x1f493;   ⏩专栏分类:Go语言专栏⏪   &#x1f69a;代码仓库:NEO的学习日记&#x1f69a;   &#x1f339;关注我&#x1faf5;带你学习更多Go语言知识   &#x1f51d;&#x1f51d; Go快速上手 1. 前言2. Go语言简介(为…

婚恋相亲交友小程序婚介红娘同城相亲交友APP源码征婚礼物线下活动

一、用户交互与活动 婚恋广场&#xff1a;用户可以在此浏览和发布自己的征婚信息。服务大厅&#xff1a;提供平台的各种服务介绍和客服支持。同城约会&#xff1a;用户可以根据地理位置找到附近的约会对象。相亲派、一线牵&#xff1a;组织相亲活动和线上配对。视频秀、真心话…

【数据结构】习题之链表中倒数第k个节点

&#x1f451;个人主页&#xff1a;啊Q闻 &#x1f387;收录专栏&#xff1a;《数据结构》 &#x1f389;前路漫漫亦灿灿 前言 今天的题目是求出链表中倒数第k个节点。 链表中倒数第k个节点 题目为&#xff1a;. - 力扣&#xff08;LeetCode&#xff09; …

Java中队列

队列是一种常见的数据结构&#xff0c;它按照先进先出&#xff08;FIFO&#xff09;的原则管理元素。在 Java 中&#xff0c;队列通常是通过链表或数组实现的&#xff0c;不同的实现类在内部数据结构和操作上可能有所不同。 1.原理 1.数据结构&#xff1a;队列的基本数据结构…

七大设计原则

在软件开发的领域中&#xff0c;随着技术的不断进步和市场需求的不断变化&#xff0c;软件系统的设计和维护变得越来越重要。为了确保软件系统能够长期有效地运行&#xff0c;并且能够在未来的发展中适应新的需求和技术变化&#xff0c;提高软件系统的可维护性和可复用性成为了…

机器学习—特征预处理和降维(四)

什么是特征预处理&#xff1f; 通过一些转换函数将特征数据转换成更加适合算法模型的特征数据过程 1包含内容 数值型数据的无量纲化&#xff1a; 归一化标准化 2特征预处理API sklearn. preprocessing为什么要进行归一化 or 标准化&#xff1f; 特征的单位或者大小相差较大…

vue源码解析——diff算法/双端比对/patchFlag/最长递增子序列

虚拟dom——virtual dom&#xff0c;提供一种简单js对象去代替复杂的 dom 对象&#xff0c;从而优化 dom 操作。virtual dom 是“解决过多的操作 dom 影响性能”的一种解决方案。virtual dom 很多时候都不是最优的操作&#xff0c;但它具有普适性&#xff0c;在效率、可维护性之…

BackTrader 中文文档(十一)

原文&#xff1a;www.backtrader.com/ 基准测试 原文&#xff1a;www.backtrader.com/docu/observer-benchmark/benchmarking/ 票号 #89 是关于添加对资产的基准测试的。理智的做法是&#xff0c;即使有一种策略&#xff0c;即使是正的&#xff0c;也低于简单跟踪资产将提供的内…

第⑭讲:Ceph集群管理:守护进程管理、日志管理和端口号配置

文章目录 1.Ceph各组件守护进程的管理方式2.守护进程管理操作2.1.Ceph所有组件的守护进程列表2.2.重启当前主机中所有的Ceph组件2.3.重启主机中所有的Monitor组件2.4.重启指定主机的Monitor组件2.5.重启指定的OSD组件 3.Ceph的日志管理4.Ceph集群各组件的守护进程5.Ceph集群各组…

All in One:Prometheus 多实例数据统一管理最佳实践

作者&#xff1a;淡唯&#xff08;啃唯&#xff09;、阳其凯&#xff08;逸陵&#xff09; 引言 Prometheus 作为目前最主流的可观测开源项目之一&#xff0c;已经成为云原生监控的事实标准&#xff0c;被众多企业广泛应用。在使用 Prometheus 的时候&#xff0c;我们经常会遇…

自然语言处理: 第二十六章大模型基底之Mistral 8x7B

文章地址: 2401.04088.pdf (arxiv.org) 项目地址: mistralai/mistral-src: Reference implementation of Mistral AI 7B v0.1 model 前言: 本文意在一文深度剖析Mistral 8X7B的关键改进点。 Mistral AI是一个由DeepMind和Meta的三位前员工在巴黎共同创立的AI公司。其在23年…

tsconfig.json文件常用配置

最近在学ts&#xff0c;因为tsconfig的配置实在太多啦&#xff0c;所以写此文章用作记录&#xff0c;也作分享 作用&#xff1f; tsconfig.jsono是ts编译器的配置文件&#xff0c;ts编译器可以根据它的信息来对代码进行编译 初始化一个tsconfig文件 tsc -init配置参数解释 …

股票开户佣金最低多少?万一!A股开户多少钱合适?

开户佣金 通常情况下&#xff0c;股票开户佣金只要在达成交易的前提才收手续的费用&#xff0c;即买入和卖出的时候。目前&#xff0c;国规定收取最高佣金的比例为千分之三。 也就是说&#xff0c;最高为成交金额的3%&#xff0c;一般都会小于这个比例。最低交易佣金是5元起&a…

mac中的VirtualBox不能分配USB设备到虚拟电脑

mac中的VirtualBox不能分配USB设备到虚拟电脑 检查工具-> 扩展包是否安装 Oracle_VM_VirtualBox_Extension_Pack-7.0.14.vbox-extpack检查usb设备是否打开 检查权限 允许VirtualBox访问&#xff1a;在“安全性与隐私”窗口中&#xff0c;选择“隐私”标签。 在左侧的列表中…

微信过期文件怎么恢复?四个高招助你轻松解决(2024新版)

“微信的文件未下载的情况下过期了&#xff0c;平时微信也没有登录在电脑上&#xff0c;之前也没有进行过数据备份&#xff0c;如何找回这个文件啊&#xff01;&#xff01;感谢回答&#xff01;” “急求&#xff0c;当时忘记点击下载&#xff0c;现在急用微信文件下载不了&a…

Go gin框架(详细版)

目录 0. 为什么会有Go 1. 环境搭建 2. 单-请求&&返回-样例 3. RESTful API 3.1 首先什么是RESTful API 3.2 Gin框架支持RESTful API的开发 4. 返回前端代码 go.main index.html 5. 添加静态文件 main.go 改动的地方 index.html 改动的地方 style.css 改动…

【Linux网络编程】TCP协议

TCP协议 1.TCP协议段格式4位首位长度序号和确认序号16位窗口大小6个标志位 2.确认应答机制3.超时重传机制4.连接管理机制如何理解连接如何理解三次握手如何理解四次挥手 5.流量控制6.滑动窗口7.拥塞控制8.延迟应答9.捎带应答10.面向字节流11.粘包问题12.TCP异常情况13.TCP小结1…

通讯录的实现(单链表版本)

我们首先要知道通讯录的实现是基于单链表的基础上的&#xff0c;所以我们首先要搞懂单链表。&#xff08;注意&#xff1a;今天的代码量较多&#xff09;&#xff0c;但这不是阻挡我们前进的脚步&#xff0c;冲冲冲&#xff01;&#xff01;&#xff01; 单链表的简要概述 我们…

剖析 SPI 在 Spring 中的应用

一、概述 SPI&#xff08;Service Provider Interface&#xff09;&#xff0c;是Java内置的一种服务提供发现机制&#xff0c;可以用来提高框架的扩展性&#xff0c;主要用于框架的开发中&#xff0c;比如Dubbo&#xff0c;不同框架中实现略有差异&#xff0c;但核心机制相同…
最新文章