C++11:lambda表达式 包装器

C++11:lambda表达式 & 包装器

    • lambda表达式
    • 包装器
      • function
      • bind


lambda表达式

在C++98中,如果想对一个结构体数组使用sort排序,那么我们就需要自己些仿函数。

比如以下结构体:

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;}
};

随着C++语法的发展,人们开始觉得上面的写法太复杂了,每次为了实现一个algorithm算法,都要重新去写一个类,如果每次比较的逻辑不一样,还要去实现多个类,特别是相同类的命名,这些都给编程者带来了极大的不便。因此,在C++11语法中出现了Lambda表达式

lambda语法如下:

[capture_list] (parameters) mutable -> return_type {statement}

这个语法看起来比较复杂,我先简单讲拆分一下各个部分:

  • [capture_list]:捕捉列表
  • (parameters):参数列表
  • mutable:一个关键字
  • -> return_type:返回值类型
  • {statement}: 函数体

比如这是一个完整的lambda表达式:

auto add = [](int a, int b)mutable -> int { return a + b; };

很明显的看出,以上函数就是传入两个整数,然后返回两数之和。

lambda表达式有很多种省略情况

  1. muteble可以省略,改关键字的具体功能后续讲解
auto add = [](int a, int b)-> int { return a + b; };
  1. 函数的返回值-> return_type可以省略,lambda表达式可以自己推导返回类型
auto add = [](int a, int b) { return a + b; };
  1. 当函数没有参数时,(parameters)参数列表可以省略
auto say_hello = [] { cout << "hello world!" << endl; };

以上函数,就已经是一个非常简单的lambda表达式了。那么lambda表达式有什么用呢?

 lambda会返回一个仿函数对象

比如auto add = [](int a, int b) { return a + b; };,其实add就是一个仿函数对象了,我们可以直接按照调用函数的方式来调用这个仿函数:add(1, 2);。但是要注意, lambda表达式返回的仿函数对象,其类名是随机的,因此必须使用auto来接受这个仿函数对象

现在我们再讲讲lambda表达式最前面的[]的作用,其名称为捕获列表,可以捕获父作用域中所有变量

比如这样:

int x = 1;
int y = 2;auto add = [x, y] {return x + y; };

以上代码中,[x, y]就是在捕获父作用域中的两个变量,那么函数体中就可以直接使用这两个变量了。如果直接通过变量名捕获,此时是传值调用,修改函数体内部的变量,不会影响父作用域的变量

但是通过直接传值捕获的变量,自带const属性,不允许修改,比如以下代码:

int x = 1;
int y = 2;auto add = [x, y] {x += 5;y += 5;};

此时代码就会报错,因为xy是通过捕获列表捕获的变量,传入的参数带有const属性,不允许修改。此时就要用到mutable了,mutable可以让被捕获的参数可以修改。

auto add = [x, y] mutable{x += 5;y += 5;};

但是这个写法还是错误的,如果使用了mutable,就算没有通过参数列表传参,()也不可以省略:

auto add = [x, y] () mutable{x += 5;y += 5;};

我们也可以以传引用的方式来捕获变量,只需要在变量名前加上&操作符:

int x = 1;
int y = 2;auto add = [&x, &y]{x += 5;y += 5;};

此时修改函数内部的xy,就是在修改父作用域的xy了。这里要注意,如果使用了传引用捕获变量,就算没有mutable也可以修改参数

另外的,lambda还提供了一次性捕获所有父作用域变量的语法,只需要在捕获列表中写=即可:

int x = 1;
int y = 2;auto add = [=]{return x + y;};

[=]就是一次性捕获了所有父作用域变量的过程,我们可以直接在函数体内部使用父作用域的所有变量。

不过[=]是以传值的形式捕获父作用域所有变量,而[&]是以传引用的形式捕获父作用域所有变量:

int x = 1;
int y = 2;auto add = [&]{x += 5;y += 5;};

另外的,我们还可以把传值和传引用混合使用,让部分参数传参,部分参数传引用。

[x, &y]:以传值的形式捕获x,以传引用的形式捕获y
[=, &x]:以传值的形式捕获父作用域所有变量,以传引用的形式捕获x
[&, x]:以传值的形式捕获x,以传引用的形式捕获父作用域所有变量

接下来我再次汇总一下lambda的语法:

各个部分:

  • [capture_list]:捕捉列表,可以捕获父作用域的任意变量,有传参和传引用两种形式
  • (parameters):参数列表,如果没有参数可以省略
  • mutable:如果以传参形式捕获参数,不可修改参数,加上该关键字后可以修改
  • -> return_type:返回值类型,可以省略,lambda会自动推导
  • {statement}: 函数体,不可省略

有了lambda表达式后,我们在需要仿函数的地方,就无需额外写一个仿函数的类,而是直接写一个lambda表达式,比如最开始的按照价格排序:

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; });

因为省略了返回值,我们以比函数还简短的方式完成了仿函数的书写。

但是有一个情况,那就是模板参数中的lambda表达式。

如果我们想要给一个优先级队列priority_queue传入一个less仿函数:

priority_queue<int, vector<int>, less<int>> q;

其中less<int>就是我们的仿函数,但是less<int>不是仿函数实例化出的对象,而是一个仿函数类型。也就是说,模板参数中需要的不是仿函数对象,而是仿函数类型。但是lambda表达式整体返回的类型是仿函数对象,因此以下写法是错误的:

priority_queue<int, vector<int>, [](const int& i1, const int& i2) {return i1 - i2; } > q;

我们不能直接把lambda当作模板参数传入,此时就要使用decltype来推导原先的类型:

auto intLess = [](const int& i1, const int& i2) {return i1 - i2; };
priority_queue<int, vector<int>, decltype(intLess)> q;

包装器

在寄快递的时候,快递会进行一次包装,这样我们就可以统一的在上面贴上快递信息,随后以统一的形式管理所有快递。包装器也是如此,包装器可以将具有相似属性的东西包装起来成为一个整体。

function

如果一个变量f,可以按照f()的形式调用函数,那么称f是一个可调用对象

回顾一下,现在我们有那些可调用对象

  1. 函数指针,函数名(函数名的本质就是函数指针)
  2. 仿函数实例化出的对象
  3. lambda表达式

这三者,都可以直接加一对()进行函数调用。它们都有各自的缺点:

  • 函数指针,函数名:类型复杂,不好用
  • 仿函数实例化出的对象:哪怕参数返回值都相同,仿函数之间的类型也不同
  • lambda表达式:类型是随机的,必须用auto接收

可以看到,这三者都有类型方面的大问题,我们也没有一种方式可以把所有参数类型和返回值类型相同的函数,统一的管理起来,让它们都变成一个类型?

包装器function就可以做到该工作,function被包含在头文件<functional>中,是一个类模板,模板原型如下:

template <class T> function;template <class Ret, class... Args>
class function<Ret(Args...)>;

其语法为:function<返回值(参数列表)>只要所有返回值和参数列表相同的可调用对象,经过这一层封装,都会变成相同的类型

比如我们现在有如下三个函数:

double func(double x)
{return x / 2;
}struct Functor
{double operator()(double x){return x / 3;}
};int main()
{auto lambadaFunc = [](double d) {return d / 4; };return 0;
}

分别是func函数,Functor仿函数,以及lambda表达式lambadaFunc 。它们的返回值都是double,参数类型也是double,因此可以经过包装器包装为function<double<double>>

如下:

function<double(double)> func1 = func;
function<double(double)> func2 = Functor();
function<double(double)> func3 = lambadaFunc;

此时,三者的类型就都是function<double(double)> 了。

有了这一层包装器,在需要统一管理函数时,就很方便了。比如说我现在要搞一个计算器的map,往map中输入哪一个操作符,就调用哪一个函数:

map<char, function<int(int, int)>> opFuncMap = {{'+', [](int x, int y) {return x + y; }},{'-', [](int x, int y) {return x - y; }},{'*', [](int x, int y) {return x * y; }},{'/', [](int x, int y) {return x / y; }}
};

由于+ - * /的函数都是lambda表达式,四个表达式的类型都是不可知的,map的第二个模板参数就不知道是啥了。不过我们可以通过function进行包装,把所有函数都包装成function<int(int, int)>类型,最后就可以通过map统一管理了。

我们最后就可以这样调用函数:

opFuncMap['+'](1, 2);
opFuncMap['-'](1, 2);
opFuncMap['*'](1, 2);
opFuncMap['/'](1, 2);

bind

bind翻译后为绑定,其可以对参数进行绑定。其主要有两个功能:改变参数顺序给指定参数绑定固定值

语法
bind是一个函数模板,其接收多个参数,第一个参数为可调用对象,后续参数为该可调用对象的参数。这个参数的语法比较特别,C++11后新增一个命名空间域placeholders,其内部会存储很多变量,这些变量用于函数的传参,变量的名字为_x表示第x个参数。

比如以下代码中:

int sub(int a, int b)
{return a - b;
}int main()
{auto f1 = bind(sub, placeholders::_2, placeholders::_1);f1(3, 5);return 0;
}

对于bind(sub, placeholders::_2, placeholders::_1);来说,sub这个参数是一个可调用对象。
placeholders::_2表示第二个参数,placeholders::_1表示第一个参数。

比如这个f1最后拿到了这个bind封装的函数,那么f1(3, 5)执行的并不是3 - 5,而是5 - 3

这是因为我们特地把placeholders::_2写在前面,f1(3, 5)把第二个5传给了placeholders::_2,把第一个3传给了placeholders::_1

而最后调用sub函数的时候,placeholders::_1会被传给sub的第一个参数,placeholders::_2则会传给sub的第而个参数。这样我们就完成了函数参数顺序的改变。

再比如以下代码:

int sub(int a, int b)
{return a - b;
}int main()
{auto f2 = bind(sub, 3.14, placeholders::_1);f2(10);return 0;
}

bind(sub, 3.14, placeholders::_1)第一个参数为可调用对象sub,第二个参数是一个固定值3.14,那么如果通过f2调用该sub函数,参数a都固定为3.14。比如f2(10)就只传了一个参数,再去调用sub时,就完成3.14 - 10的操作。因此我们可以通过sub把某个参数绑定为固定值。


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

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

相关文章

Qt 总结

由于工作需要用到Qt。把过程中学习到的东西记录下来&#xff0c;希望能帮到他人和将来的自己。 由于需要快速实现需求&#xff0c;所以对Qt只是使用&#xff0c;并没有对原理的深入理解。 故此文只适合入门&#xff0c;不适合深入学习Qt。 文章目录 安装&维护示例&教…

案例:非功能性需求的设计

在咨询中看到很多项目组对于非功能性需求没有做设计&#xff0c;很多项目组在设计文档中仅仅是把非功能性需求的描述拷贝到设计文档的非功能性章节。因此特地设计了两个简单的需求给大家参考&#xff0c;希望能够引导设计人员重视非功能性需求的设计。

视觉大模型--deter的深入理解

但对于transformer用于目标检测领域的开创性模型&#xff0c;该模型言简意赅&#xff0c;但是但从论文理解&#xff0c;有很多细节都不清楚&#xff0c;尤其是解码器的query和二分图匹配(Bipartite Matching)和匈牙利算法(Hungarian Algorithm)相关&#xff0c;本文将根据代码详…

32. UE5 RPG使用增强输入激活GameplayAbility(二)

在上一篇文章中&#xff0c;我们实现了Tag和InputAction的数据对应&#xff0c;后面&#xff0c;我们会通过InputAction触发对应的Tag&#xff0c;然后在GameplayAbility身上设置对应的Tag&#xff0c;然后通过Tag遍历角色身上的所有应用的技能去激活。为了实现这个功能&#x…

P8597 [蓝桥杯 2013 省 B] 翻硬币

# [蓝桥杯 2013 省 B] 翻硬币 ## 题目背景 小明正在玩一个“翻硬币”的游戏。 ## 题目描述 桌上放着排成一排的若干硬币。我们用 * 表示正面&#xff0c;用 o 表示反面&#xff08;是小写字母&#xff0c;不是零&#xff09;&#xff0c;比如可能情形是 **oo***oooo&#x…

【C++基础】std::vector详解

std::vector 是 C 标准库中的一个容器&#xff0c;提供了动态数组的功能。它的底层实现通常是使用连续的内存块来存储元素&#xff0c;因此可以通过指针算术来访问元素&#xff0c;并且支持常数时间的随机访问&#xff0c;并支持在容器末尾高效地添加和删除元素。 一、底层实现…

精读 Generating Mammography Reports from Multi-view Mammograms with BERT

精读&#xff08;非常推荐&#xff09; Generating Mammography Reports from Multi-view Mammograms with BERT&#xff08;上&#xff09; 这里的作者有个叫 Ilya 的吓坏我了 1. Abstract Writing mammography reports can be errorprone and time-consuming for radiolog…

基于单片机的数字万用表设计

**单片机设计介绍&#xff0c;基于单片机的数字万用表设计 文章目录 一 概要二、功能设计设计思路 三、 软件设计原理图 五、 程序六、 文章目录 一 概要 基于单片机的数字万用表设计概要是关于使用单片机技术来实现数字万用表功能的一种设计方案。下面将详细概述该设计的各个…

从零学算法80

80. 删除有序数组中的重复项 II 给你一个有序数组 nums &#xff0c;请你 原地 删除重复出现的元素&#xff0c;使得出现次数超过两次的元素只出现两次 &#xff0c;返回删除后数组的新长度。 不要使用额外的数组空间&#xff0c;你必须在 原地 修改输入数组 并在使用 O(1) 额外…

【性能测试】接口测试各知识第2篇:学习目标,1. 理解接口的概念【附代码文档】

接口测试完整教程&#xff08;附代码资料&#xff09;主要内容讲述&#xff1a;接口测试&#xff0c;学习目标学习目标,2. 接口测试课程大纲,3. 接口学完样品,4. 学完课程,学到什么,5. 参考:,1. 理解接口的概念。学习目标&#xff0c;RESTFUL1. 理解接口的概念,2.什么是接口测试…

ChatGPT 的行家指南

原文&#xff1a;An Insider’s Guide to using ChatGPT 译者&#xff1a;飞龙 协议&#xff1a;CC BY-NC-SA 4.0 介绍 你是否厌倦了花费无数小时为你的业务创建内容&#xff1f;从博客文章到社交媒体更新&#xff0c;从电子书内容到电子邮件&#xff0c;这可能是一个耗时的过…

Kafka面试宝典

1 Kafka基础面试篇 Kafka的那些设计让它有如此高的性能? 1.partition,producer和consumer端的批处理:提高并行度;2.页缓存:大量使用页缓存,内存操作比磁盘操作快很多,数据写入直接写道页缓存,由操作系统负责刷盘,数据读取也是直接命中页缓存,从内存中直接拿到数据;…

如何保持数据一致性

如何保持数据一致性 数据库和缓存&#xff08;比如&#xff1a;redis&#xff09;双写数据一致性问题&#xff0c;是一个跟开发语言无关的公共问题。尤其在高并发的场景下&#xff0c;这个问题变得更加严重。 问题描述&#xff1a; 1.在高并发的场景中&#xff0c;针对同一个…

基于java+SpringBoot+Vue的学生心理咨询评估系统设计与实现

基于javaSpringBootVue的学生心理咨询评估系统设计与实现 开发语言: Java 数据库: MySQL技术: Spring Boot MyBatis工具: IDEA/Eclipse、Navicat、Maven 系统展示 后台展示 用户管理模块&#xff1a;管理员可以查看、添加、编辑和删除用户信息。 试题管理模块&#xff1a…

Qt + VS2017 创建一个简单的图片加载应用程序

简介&#xff1a; 本文介绍了如何使用Qt创建一个简单的图片加载应用程序。该应用程序可以打开图片文件并在界面上显示选定的图片&#xff0c;并保存用户上次选择的图片路径。 1. 创建项目&#xff1a; 首先&#xff0c;在VS中创建一个新的Qt Widgets应用程序项目&#xff0c;并…

LeetCode 1379.找出克隆二叉树中的相同节点:二叉树遍历

【LetMeFly】1379.找出克隆二叉树中的相同节点&#xff1a;二叉树遍历 力扣题目链接&#xff1a;https://leetcode.cn/problems/find-a-corresponding-node-of-a-binary-tree-in-a-clone-of-that-tree/ 给你两棵二叉树&#xff0c;原始树 original 和克隆树 cloned&#xff0…

Golang | Leetcode Golang题解之第3题无重复字符的最长子串

题目&#xff1a; 题解&#xff1a; func lengthOfLongestSubstring(s string) int {// 哈希集合&#xff0c;记录每个字符是否出现过m : map[byte]int{}n : len(s)// 右指针&#xff0c;初始值为 -1&#xff0c;相当于我们在字符串的左边界的左侧&#xff0c;还没有开始移动r…

Linux初学(十二)AWK进阶

一、AWK 1.1 简介 AWK是Linux中重要的文本处理工具Linux三剑客只一处理的对象可以是一个具体的文件&#xff0c;也可以是一个命令的执行结果AWK按行读取文件&#xff0c;将每一行视为一条记录 案例一&#xff1a;获取系统中每个用户的uid 方法一&#xff1a;cat /etc/passwd |…

vue3+threejs新手从零开发卡牌游戏(二十五):尾声(附完整源码下载地址)

这个demo到这里就算接近尾声了&#xff0c;大体的游戏框架就算搭建完成了&#xff0c;主要是提供了一下思路&#xff0c;代码也是来来回回修改了好几次&#xff0c;也踩了一些坑&#xff0c;后续可以自行优化一些战斗效果和交互逻辑、UI美化等&#xff0c;这里附上源码下载链接…

CMD 命令行进入到电脑硬盘的某个目录的几种方式

本文介绍几种 cmd 命令行进入到电脑硬盘的某个目录的几种方式。 1、在具体文件目录地址栏输入 cmd 回车 这是最快的、最牛的方式&#xff0c;没有之一。 比如&#xff1a;我想进入一个层级很深的文件目录&#xff0c;直接打开在那个目录&#xff0c;把地址栏信息删除清空&am…