【C++深陷】之“lambda表达式”

2023-11-13

0. 什么是lambda表达式

《C++ Primer(第5版)》对 lambda表达式(lambda expression) 的定义为:

一个lambda表达式表示一个可调用的代码单元。我们可以将其理解为一个未命名的内联函数。

lambda表达式的形式如下:

[capture list](parameter list) -> return type { function body }
  • 捕获列表,capture list
  • 参数列表,parameter list
  • 返回类型,return type
  • 函数体,function body

lambda表达式是可调用对象(callable object 的一种。

1. 如何使用lambda表达式

本节将宏观的介绍几种lambda表达式的用法,对整体有一个认知。

首先介绍谓词——lambda最广泛的应用之一,然后介绍如何向谓词传递lambda表达式以及简单的使用捕获列表。

1.1 向函数传递谓词

谓词(predicate) 是一个可调用的表达式,其返回结果是一个能用作条件的值。

C++标准库中的泛型算法大多使用了谓词,允许我们自己定义一些操作。例如:

bool shorter(const string &s1, const string &s2) {
    return s1.size() < s2.size();
}

// 假设A是一个包含string的vector
std::sort(A.begin(), A.end(), shorter)
// sort会根据A中每个string的size来排序

std::sort的第三个参数,就是一个谓词类型。我们传递给std::sort的是函数名shorter,因为函数也是一个可调用对象

std::sort会使用shorter定义的操作代替<,来比较容器中的元素进行排序。

从一定程度上说,谓词帮助我们拓展了函数的功能,定制操作。若按<来排序string,仅仅是按照字母表排序;现在可以使用字符串的长度来排序,甚至是先按照长度,再按照字母表。

谓词分为一元谓词二元谓词,即在调用谓词时传递给谓词的参数是1个还是2个。

谓词可调用对象的区别就是,谓词是一个约束了参数列表和输出类型的可调用对象,在向谓词传递可调用对象时必须符合谓词的约束。比如必须传递两个参数,返回布尔类型(这一点和运算符的感觉比较像)。

1.2 lambda表达式可以作为谓词

可调用对象一共有四种,因此向谓词传递函数函数指针重载了函数调用运算符的类lambda表达式都可以。本文的重点是lambda表达式。

我们使用lambda表达式将shorter函数重写:

std::sort(A.begin(), A.end(),
          [](const string &a, const string &b)
          -> bool
          { return a.size() < b.size(); });

我们列出第0节介绍的lambda表达式的四部分:

  • 捕获列表,这里为空[]
  • 参数列表,两个参数,const string &a, const string &b
  • 返回类型,bool类型
  • 函数体,return a.size() < b.size();

OK,lambda表达式就是这么写。

当然还可以举一个传递一元谓词的例子,比如find_if

// 代码获得A中第一个长度>=3的元素的迭代器
auto res = std::find_if(A.begin(), A.end(),
                        [](const string &a)
                        -> bool
                        { return a.size() >= 3; });
  • 捕获列表,这里为空[]
  • 参数列表,一个参数,const string &a
  • 返回类型,bool类型
  • 函数体,return a.size() >= 3;

1.3 省略更优美

进一步,就是lambda表达式的省略形式:

  1. 参数列表可以省略
  2. 返回类型可以省略(通常都省略)
// 省略参数列表
auto f = [] { return 42; };

// 省略返回类型
auto res = std::find_if(A.begin(), A.end(),
                        [](const string &a)
                        { return a.size() >= 3; });

这里假如我想返回的是长度大于等于某个变量sz的值,该怎么修改?

1.4 使用捕获列表

写成return a.size() >= sz;?

sz哪里来的呢?用参数传递进来可以吗?写成下面这样试试:

[](const string &a, int sz) -> bool { return a.size() >= sz; }

这样定义一个lambda表达式没问题,只不过这个表达式不能在find_if里面使用了,因为find_if约束,最后一个谓词类型的参数列表只能有一个参数。

此时就要用到捕获列表了。

通过捕获列表,我们可以在lambda表达式中,使用上层作用域的局部非static变量

还是find_if

vector<string>::iterator find_sz_bigger(vector<string> &A, int sz) {
    return std::find_if(A.begin(), A.end(),
                        [sz](const string &a)
                        -> bool
                        { return a.size() >= sz; });
}

这儿的重点就是sz

在上层作用域find_sz_bigger函数中,我们有一个形参sz,它是一个局部变量,函数调用结束后它就被销毁了。

在lambda表达式的捕获列表中,我们填写了一个sz,意思就是捕获了find_sz_bigger作用域中名为sz对象的值,在lambda表达式的函数体中,我们可以使用sz

注意此处的捕获方式,是把sz的值拿了过来,即按值传递(当然还有别的捕获方式,见第2节)。

1.5 小结

到目前你已经学会了如何简单的使用lambda表达式了。

不仅如此,你还了解了如下内容:

  • 谓词,std中的算法很多都使用,如find_ifsort
  • 可调用对象,只有四种
  • lambda表达式,一种可调用对象,给std中的算法传递谓词很方便
  • 捕获列表,这是lambda表达式和其他的可调用对象区别比较大的地方
  • 省略形式更优美

接下来我们来看看lambda表达式的细节,以及注意事项。

2. 深入捕获列表

捕获列表指引lambda在其内部包含访问局部变量所需的信息。

捕获列表捕获变量的方式有三种:

  1. 值捕获
  2. 引用捕获
  3. 隐式捕获

2.1 值捕获

与函数调用值传递类似,采用值捕获的前提是变量可以拷贝。与函数参数不同,被捕获的变量的值是在lambda创建时拷贝,而不是调用时拷贝

例如:

void fcn1() {
    size_t v1 = 42;
    auto f = [v1] { return v1; };
    v1 = 0;
    auto j = f();  // j为42
}

在lambda创建时拷贝,随后我们修改v1的值,不会影响f()调用的结果。

值传递还包括指针的情况,因此若捕获的是某个对象的指针,一定要注意指针的指向一定是有效的。

使用值捕获得到的对象(例如v1),我们不能在lambda函数体中修改它的值。

若希望修改它的值,必须使用mutable关键字修饰lambda表达式。

void fcn2() {
    size_t v1 = 42;
    auto f = [v1]() mutable { return ++v1; };
    v1 = 0;
    auto j = f();  // j为43
}

此时的lambda称为可变lambda,形参列表不能省略()

2.2 引用捕获

一个以引用方式捕获的变量与其他任何类型的引用的行为类似:

void fcn3() {
    size_t v1 = 42;
    auto f = [&v1] { return v1; };
    v1 = 0;
    auto j = f();  // j为0
}

如果我们采用引用方式捕获一个变量,就必须确保被引用的对象在lambda执行的时候是存在的

lambda捕获的都是局部变量,在上层代码块结束后局部变量就会被销毁,因此使用lambda表达式捕获引用变量一定要注意。

引用捕获的必要性在于捕获那些不能拷贝的对象,例如ostream

使用引用捕获得到的对象,可不可以修改要根据源对象是否有const

2.3 隐式捕获

除了上述2种方式,我们显示的在捕获列表中说明捕获的对象(显示捕获),还可以让编译器根据lambda函数体中的代码来推断我们要使用哪些变量,这就是隐式捕获

为了指示编译器推断捕获列表,需要在捕获列表中写一个=或者&,分别代表函数体中使用的变量捕获的方式为值捕获或者引用捕获

例如之前的例子:

int sz = 3;

// 函数体中出现了sz
// 捕获列表中使用了 =
// 函数体中sz使用值捕获
auto it = std::find_if(A.begin(), A.end(),
                       [=](const string &a)
                       { return a.size() >= sz; });

还可以混合显示捕获隐式捕获

  1. 捕获列表第1个参数放置=或者&,代表默认捕获方式
  2. 捕获列表其余的参数,均采用和默认捕获方式相异的方式。

例如:

ostream &os = std::cout;
char c = ' ';

// os采用默认的引用捕获
// c采用值捕获
std::for_each(A.begin(), A.end(),
              [&, c](const string &s)
              { os << s << c; });

// c采用默认的值捕获
// os采用引用捕获
std::for_each(A.begin(), A.end(),
              [=, &os](const string &s)
              { os << s << c; });

注意:引用捕获不论何时都需要&标志,如上面的第二个例子,即使默认是值捕获,其后的参数也要声明&

2.4 小结

捕获方式分为显示捕获隐式捕获混合式捕获

显示捕获包括值捕获引用捕获

隐式捕获使用=或者&设置函数体中所有对象的捕获方式为值捕获或者引用捕获。

混合式捕获第一个参数放置默认捕获方式,其余参数与默认捕获方式相异

值捕获得到的对象在lambda函数体中不可以修改,可以使用mutable

尽量避免引用捕获

3. 深入返回类型

3.1 省略时一定只有return语句

lambda表达式的返回类型通常都是省略的,这是因为lambda表达式面向的编程需求,就是函数体尽可能的短小精悍(若函数功能复杂,尽量使用其他可调用对象来实现)、一目了然的。

但是注意,如果我们一边省略了返回类型,一边在lambda函数体中包含了return语句之外的任何语句,编译器会假定此lambda返回void

例如:

// 假设vi是vector<int>
// 这是一个正确的例子
std::transform(vi.begin(), vi.end(), vi.begin(),
               [](int i)
               { return i < 0 ? -i : i; });

上述代码实现了将vi中的所有元素取绝对值。

三元运算符的等价形式是if else语句:

// ERROR: 这是一个错误的例子
std::transform(vi.begin(), vi.end(), vi.begin(),
               [](int i)
               { if (i < 0) return -i; else return i; });

上述代码产生编译错误,错误的原因是transform的第四个参数类型不匹配谓词

不匹配的原因是lambda表达式的返回结果是void

返回是void的原因是我们省略了返回类型,在函数体中书写了return语句之外的内容,编译器推断lambda的返回类型为void

此时必须显示定义返回类型:(尾置形式)

// 这是一个正确的例子
std::transform(vi.begin(), vi.end(), vi.begin(),
               [](int i) -> int
               { if (i < 0) return -i; else return i; });
// 但是有三元运算符,
// 尽量不要使用if else分支
// 让程序执行效率更高

上述代码是对的,但是尽量使用三元运算符。

返回类型可以是值类型,也可以是引用类型。

返回引用类型也需要注意调用点处返回的对象必须要存在。

4. 编译器如何看待lambda

4.1 两个“未命名”

我们看到的lamdba表达式是由[]()->以及{}组合起来的漂亮形式;编译器却为lambda表达式生成了一个对应的新的未命名类类型,并且把它变成了一个该未命名类类型的未命名函数对象

这里的关键词,我们逐个解析:

  • 未命名类类型:类类型都知道,就是class
  • 未命名函数对象:如果一个类定义了调用运算符,那么该类的对象就称作函数对象

现在明白了,回想可调用对象,其中有一个就是重载了函数调用运算符的类,它的官名就是函数对象

意思就是编译器将lambda编译成了一个新类型,和新类型的对象

我们可以将之前的例子改写一下,例如:

std::sort(A.begin(), A.end(),
          [](const string &a, const string &b)
          { return a.size() < b.size(); });

它的未命名类类型是:

class XXXXXXX {
public:
    bool operator() (const string &a, const string &b) const
    { return a.size() < b.size(); }
}

std::sort(A.begin(), A.end(), XXXXXXX());

虽然定义上说lambda表达式可以被理解为“一个未命名的内联函数”,但实际编译器却使用函数对象来实现。

现在真相大白了。

4.2 捕获列表是什么

捕获列表是如何变身为函数对象中的内容的?

引用捕获不需要变为函数对象中的数据成员,在调用lambda时,编译器直接将引用源的值拿来使用(这也是引用的意义,外部改变了lamdba内部也要改变)。

值捕获会创建一个数据成员,以及初始化该数据成员的构造函数。

例如:

int sz = 3;
auto it = std::find_if(A.begin(), A.end(),
                       [sz](const string &a)
                       { return a.size() >= sz; });

它的未命名类类型是:

class XXXXXXX {
private:
    int sz;
public:
    XXXXXXX(int x1): sz(x1) { }
    bool operator() (const string &a, const string &b) const
    { return a.size() < b.size(); }
}

auto it = std::find_if(A.begin(), A.end(), XXXXXXX(sz));

这也是值捕获的意义,在创建时刻捕获到程序中某些值,不论外部如何修改,我只需要那个时刻的值。

因此在2.4节中提到的“尽量避免引用捕获”也是出于此分析。

现在真相大大白了。

4.3 合成的函数还有吗

我们知道,若一个类没有默认构造函数、析构函数、以及赋值运算符,C++编译器会为类合成默认的相关函数。

但是lambda表达式产生的类没有。

它是否含有默认的拷贝构造函数、移动构造函数会根据捕获的数据成员类型而定。

4.4 mutable lambda

使用mutable修饰的lambda表达式,在转换为函数对象时,只需要把调用运算符const去掉即可。

5. 总结

一个lambda表达式表示一个可调用的代码单元。它由捕获列表参数列表返回类型函数体四部分组成。

捕获列表可以捕获上层作用域中的对象的内容。

参数列表决定调用lambda表达式时所需的参数。

返回类型通常省略,但是注意除return语句之外的情况。

函数体通常精悍短小、一目了然。

可以使用mutable修饰lamdba表达式,表明可以修改值捕获的对象的值。

lambda表达式是可调用对象的一种,编译器把它转换为未命名类类型的未命名函数对象来实现其功能。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

【C++深陷】之“lambda表达式” 的相关文章

随机推荐

  • Pytorch基础学习(第一章-PyTorch基础概念)

    课程一览表 目录 一 pytorch简介 二 环境配置 1 pycharm 2 annaconda 3 安装pytorch
  • altium designer芯片引脚间距规则过小

    AD中芯片的引脚间距过小 例如stm32这种MCU 引脚又细又密 违反了默认间距规则 如上图所示的16mil 而触发绿色的报错 但是我们又不能因噎废食 而把整个PCB规则间距改大 因此最好的解决方案是 只修改这一个芯片的间距规则 依次点击
  • 云汉芯城js逆向分析-v,t,s参数

    介绍 查看搜索的接口 很明显需要这几个参数 keyword 2N7002 搜索输入的关键词 font ident 945a41f33fc9693c 如下图 第一次访问的页面 返回的html里找 v 1655799627669 时间戳 t 1
  • Android平台GB28181设备接入模块相关博客概览

    Android平台GB28181设备接入模块 可实现不具备国标音视频能力的 Android终端 通过平台注册接入到现有的GB T28181 2016服务 可用于如智能监控 智慧零售 智慧教育 远程办公 生产运输 智慧交通 车载或执法记录仪等
  • 从零开始 verilog 以太网交换机(六)帧处理单元设计与实现

    从零开始 verilog 以太网交换机 六 帧处理单元设计与实现 声明 博主主页 王 嘻嘻的CSDN主页 从零开始 verilog 以太网交换机系列专栏 点击这里 未经作者允许 禁止转载 侵权必删 关注本专题的朋友们可以收获一个经典交换机设
  • 数据结构——栈(stack)

    一 顺序栈 栈 stack 是一种运算受限的线性表 其限制是仅允许在表的一端进行插入和删除运算 这一端被称为栈顶 相对地 把另一端称为栈底 向一个栈插入新元素又称作进栈 入栈或压栈 它是把新元素放到栈顶元素的上面 使之成为新的栈顶元素 从一
  • java中如何从一个url的字符串中提取出ip、port等信息

    欢迎访问个人博客 德鲁大叔撸代码 项目中有一个功能是 把我生成的对账单推送到商户指定的sftp服务器上 要上传文件到sftp那必须的就有以下几个数据 sftp的ip sftp所指向的port sftp的用户名 sftp的密码 指定sftp上
  • 安装HP LaserJet 1320n打印机驱动

    该打印机型号比较老了 不是网络打印机 只能通过并口或USB安装 由于笔记本不带并口 现在台式机基本都不带了 只能选择USB安装 折腾了好久 才找到正确的安装方法 特分享给大家 首先 就是找到正确的驱动 我的笔记本是win8 64bit的 所
  • Ioc容器refresh总结(4)--- Spring源码从入门到精通(三十四 )

    上偏文章介绍了 registerBeanPostProcessor 分别按优先级顺序先注册PriorityOrdered和Ordered接口 第三部注册没有实现接口的beanPostProcessor 最后注册mergedBeanDefin
  • 特征选取1-from sklearn.feature_selection import SelectKBest

    sklearn实战 乳腺癌细胞数据挖掘 博主亲自录制视频 https study 163 com course introduction htm courseId 1005269003 utm campaign commission utm
  • 从入门到入土:[SEED-Lab]-SQL注入攻击

    此博客仅用于记录个人学习进度 学识浅薄 若有错误观点欢迎评论区指出 欢迎各位前来交流 部分材料来源网络 若有侵权 立即删除 本人博客所有文章纯属学习之用 不涉及商业利益 不合适引用 自当删除 若被用于非法行为 与我本人无关 SEED Lab
  • Flex 学习资源

    Action Script 3 0 帮助 http help adobe com zh CN ActionScript 3 0 ProgrammingAS3 Flex 实例 http blog minidx com 2009 04 06 2
  • 计算机文献汇报ppt,常见的研究生文献汇报.ppt

    常见的研究生文献汇报 Example two Fig 9 shows a schematic illustration of the synthesis routes of single molecular nano particles m
  • 《Android 开发艺术探索》笔记7--RemoteViews的内部机制和意义

    RemoteViews的内部机制和意义思维导图 RemoteViews的内部机制 RemoteViews的意义可以模拟一个通知栏效果并实现跨进程的UI更新 参看文章 RemoteViews的内部机制和意义思维导图 RemoteViews的内
  • hadoop MapReduce总体工作机制简述

    问题导读 1 如何理解MapTask运行机制 2 如何理解Map阶段机制 3 如何理解ReduceTask 工作机制 4 如何理解MapReduce总体工作机制 MapTask运行机制详解整个Map阶段流程大体如图所示简单概述inputFi
  • @EnableGlobalMethodSecurity注解详解

    作用 当我们想要开启spring方法级安全时 只需要在任何 Configuration实例上使用 EnableGlobalMethodSecurity 注解就能达到此目的 同时这个注解为我们提供了prePostEnabled secured
  • python把tex转为html,在Python中将特定符号替换为字符串,将LaTeX转换为HTML,而无需使用转换器...

    如果要替换所有 非重叠 实例 则可以执行以下操作 import re re sub r r 1 w 这将用 lt sub gt 除 之外的所有东西 sub找到 除 之外的所有东西 的每个实例 这意味着 如果您有类似Li 3 O cat 的东
  • Docker daemon及容器实例的DNS配置详解

    Docker daemon及容器实例的DNS配置详解 2022 01 22 18 37 180阅读 0赞 Linux系统中 DNS解析器是一组C库的进程 用以访问DNS服务器 DNS解析器通过 etc resolv conf配置文件给出要访
  • Latex三线表绘制横线

    目前遇到两种方式 toprule midrule bottomrule usepackage booktabs begin table htb caption Table caption label t1 begin tabular ll
  • 【C++深陷】之“lambda表达式”

    0 什么是lambda表达式 C Primer 第5版 对 lambda表达式 lambda expression 的定义为 一个lambda表达式表示一个可调用的代码单元 我们可以将其理解为一个未命名的内联函数 lambda表达式的形式如