C++11:lambda表达式

news2024/11/15 8:36:49

文章目录

  • 1. 概念
  • 2. 语法
  • 3. 示例
    • 示例1
    • 示例2
    • 示例3
    • 示例4
  • 4. 捕捉方式
    • 基本方式
    • 隐式和混合
    • 补充
  • 5. 传递lambda表达式
    • 示例
  • 6. 原理
  • 7. 内联属性

1. 概念

lambda表达式实际上是一个匿名类的成员函数,该类由编译器为lambda创建,该函数被隐式地定义为内联。因此,调用lambda表达式相当于直接调用它的operator()函数,这个函数可以被编译器内联优化(建议)。

例如快速排序算法,STL允许用户自定义比较方式,在C++11之前,通常使用仿函数实现。但是代码一旦很长,使用之处和仿函数实现的地方相隔甚远,而且如果仿函数的命名不规范,很容易造成使用上的困难。

仿函数,又叫做函数对象(functor,function object),因为实现仿函数的方式就是重载一个类的operator(),只是用起来跟函数一样,其本质依然是一个对象。

2. 语法

C++11的lambda表达式是一种允许内联函数的特性,它可以用于不需要重用和命名的代码片段。lambda表达式的一般形式是:

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

以一个简单的例子理解lambda表达式的语法:

[]
{
    cout << "hello lambda" << endl;
}

这一大坨就表示它是一个类型/对象(type、class),我们知道,在类型后加上(),就表示调用这个对象的构造函数,**而lambda的语法有些不同,在它后面加上括号,就表示调用它。**就像:

#include <iostream>
using namespace std;

int main()
{
    []
    {
        cout << "hello lambda" << endl;
    }();
    
    return 0;
}

输出:

hello lambda

而一般没有人会这样调用lambda表达式,因为这样还不如直接写中间的语句,所以可以暂时给它一个名字:

auto l = []
{
    cout << "hello lambda" << endl;
};

l在这里是任意取的名字(实际上要根据含义),刚才提到,方括号和后面一坨东西,就相当于一个对象,一般情况下我们是不知道它的类型的,所以通常用auto接收。

现在,最简形式的lambda表达式就写出来了,试着调用它:

#include <iostream>
using namespace std;


int main()
{
    auto l = []
    {
        cout << "hello lambda" << endl;
    };
    l();
    return 0;
}

输出:

hello lambda

注意:

  • 上面的例子和概念中的“内联”属性并没有关系。
  • lambda表达式在main函数之外定义也不影响结果。
  • lambda表达式的最后有分号。
  • lambda函数的参数列表和返回值类型不是必要的,但捕捉列表和函数体是不可省略的。习惯上不写返回值,让表达式自动推导。

3. 示例

现在,lambda表达式的基本用法已经有大概的了解了,下面将介绍它的细节。

示例1

int main()
{
    int id = 0;
    auto f = [id] () mutable
    {
        cout << "id:" << id << endl;
        ++id;
    };
    id = 42;
    f();
    f();
    f();

    cout << "id:" << id << endl;
}

注:在这里先不管mutable

  • [id]表示在lambda表达式外部的所有名为id的变量都能被使用
  • ()表示这个lambda表达式不用传参
  • 函数内部的语句打印变量id的值且累加
  • 在更新变量id的值为42后,调用3次lambda表达式f
  • 最后再打印变量id的值

输出:

id:0
id:1
id:2
id:42

这与设想的结果应该不同:既然变量id的值已经被更新为42,那么lambda表达式f打印出的值应该是42、43、44,这是为何呢?这个问题会在看完所有例子以后揭晓答案。

通过例1,最重要的是理解lambda的本质是一个仿函数,例1的lambda表达式等价于下面的代码:

image-20230301082521983

  • [id]中没有任何特殊符号,表示默认以传值方式捕捉变量,也就相当于拷贝一份变量id的副本到这个匿名函数中,且捕捉后不再受外界变量id的影响
  • mutable表示可以修改捕捉的变量,那么也就可以执行自增语句
  • 例1按照语句的执行顺序,在执行lambda表达式时,变量id的值为0时就已经被捕捉了,即使它被更新为42,后续调用lambda表达式时变量id仍然从0开始

但是,即使lambda表达式内部对变量id自增,最终变量id仍然为最后一次修改的值:42。

示例2

int main()
{
    int id = 0;
    auto f = [&id] (int param)
    {
        cout << "id:" << id << ", ";
        cout << "param:" << param << endl;
        ++id;
        ++param;
    };

    id = 42;
    f(7);
    f(7);
    f(7);

    cout << "id:" << id << endl;
    return 0;
}

输出:

id:42, param:7
id:43, param:7
id:44, param:7
id:45

例2和例1的区别:去掉了mutable,以传引用方式捕捉变量,新增参数param。

  • [&id],表示以引用方式捕捉变量id。那么匿名函数内部和外界处理的变量id就是同一个,函数内部和外部都会互相影响。因此在调用lambda表达式之前变量id被更新为42仍然会影响lambda表达式内部。
  • 参数param是值传递,每次传值都是一次临时拷贝。

示例3

int main()
{
    int id = 0;
    auto f = [id] ()
    {
        cout << "id:" << id << ", ";
        ++id; // 编译错误
    };

    id = 42;
    f();
    f();
    f();

    cout << "id:" << id << endl;
    return 0;
}

在自增语句编译错误:

error: cannot assign to a variable captured by copy in a non-mutable lambda

提示不能赋值给一个非可变的(non-mutable)变量。结合例1,言外之意是mutable的修饰使得lambda表达式能够在内部修改捕捉到的变量。

示例4

int main()
{
    int id = 0;
    auto f = [id] () mutable
    {
        cout << "id:" << id << endl;
        ++id;

        static int x = 5;
        int y = 6;
        return id;
    };

    f();
    return 0;
}

此例仅为了说明lambda表达式本质是一个匿名函数(底层由仿函数实现),所有函数的内部都能定义不同类型的变量,也允许有返回值。

4. 捕捉方式

基本方式

Lambda表达式的捕捉方式是指它如何访问外部变量,也就是定义在lambda表达式之外的变量。Lambda表达式的最基本的两种捕获方式是:按值捕获(Capture by Value)和按引用捕获(Capture by Reference)。

  • 按值捕获是指lambda表达式内部保存一份外部变量的副本,对这个副本进行操作不会影响原来的变量。

  • 按引用捕获是指lambda表达式内部直接使用外部变量的引用,对这个引用进行操作会影响原来的变量。

隐式和混合

除此之外,还有两种方式:隐式捕获和混合方式。它们是两种更简便的捕获方式,它们可以让编译器自动推断需要捕获的外部变量。

隐式捕获:

  • [=]表示以值捕获的方式捕获所有外部变量(默认),成员函数包括this指针;
  • [&]表示以引用捕获的方式捕获所有外部变量,成员函数包括this指针。

混合方式是指在隐式捕获的基础上,显式地指定某些变量的不同捕获方式。

混合使用时,要求捕获列表中第一个元素必须是隐式捕获(&或=),然后后面可以跟上显式指定的变量名和符号。

混合使用时,若隐式捕获采用引用捕获&,则显式指定的变量必须采用值捕获的方式;若隐式捕获采用值捕获=,则显式指定的变量必须采用引用捕获的方式,即变量名前加&。

一个例子:

int main()
{
    int a = 123;
    int b = 456;
    auto f = [=, &b]() { // 隐式值捕获a,显式引用捕获b
        cout << a << endl; // 输出:123
        cout << b << endl; // 输出:456
        b = 789; // 修改b的值
    };
    f();
    cout << b << endl; // 输出:789
    
    return 0;
}

输出:

123
456
789

补充

  • [=]以值捕获,是默认状态一般不写。

  • 不允许以相同方式重复捕捉,例如[=, a],前者已经表示以值捕获所有外部变量,后者就重复了。但是[=, &a]是被允许的,因为它们的捕获方式不同。

  • Lambda表达式的父作用域是指定义lambda表达式的那个作用域,通常是一个函数或者一个类。Lambda表达式的块作用域是指lambda表达式本身的作用域,它是一个匿名函数,可以在其他地方调用。

  • Lambda表达式可以捕获父作用域中的局部变量,但是不能捕获其他作用域或者非局部变量。Lambda表达式捕获父作用域中的局部变量时,要求这个变量必须是声明为final的,或者实际上是final的(即不会被修改)。Lambda表达式不能在自己的块作用域中声明和父作用域中同名的参数或者局部变量。

  • Lambda表达式之间不能相互赋值,因为它们的本质是函数对象。即使从文本层面看它们的名字相同,但是底层编译器给它们取的名字是不同的。

5. 传递lambda表达式

C++传递lambda表达式的方法有以下几种:

  • 将lambda表达式转换为相应的函数指针(如果没有捕获任何变量)。使用std::function作为参数类型,它可以接受任何可调用对象(包括捕获变量的lambda表达式)。例如:
void foo(std::function<bool(int)> f) 
{
  // 函数动作
}

foo(
    [] (int x) // 传递lambda表达式
    { return x > 0; 
    }
); 
  • 将lambda表达式赋值给一个变量,然后将这个变量传递给函数。例如:
auto add = [] (int a, int b) // 将lambda表达式赋值给add变量
{ 
    return a + b; 
}; 

int result = add(1, 2); // 调用lambda表达式
bar(add); // 传递lambda表达式给变量bar
  • 使用decltype来推导出lambda表达式的类型,然后将这个类型作为模板参数传递给函数。例如:
template<typename F>
void baz(F f) 
{
  // 函数动作
}

auto mul = [] (int a, int b) 
{
    return a * b; 
}; // 定义lambda表达式

baz<decltype(mul)>(mul); // 传递lambda表达式和它的类型给变量baz

示例

下面以第三种方式为例:

int main()
{
    auto cmp = [](int a, int b)
    {
        return a > b;
    };

    set<int, decltype(cmp)> s(cmp);
    
    return 0;
}

这段代码中使用了decltype是为了推断出lambda表达式的类型。decltype是一个C++11引入的关键字,它可以根据表达式的类型返回一个类型。在这个例子中,decltype(cmp)就是lambda表达式的类型,它被用作set容器的第二个模板参数,指定了set中元素的比较方式。set的第三个模板参数需要一个比较函数类型,而lambda表达式本身没有一个固定的类型,decltype可以根据lambda表达式的operator()来推导出它的函数类型,从而满足set的要求。

如果不使用decltype,就需要显式地写出lambda表达式的类型,但这很麻烦,因为lambda表达式的类型是编译器生成的,并没有一个具体的名字。你就需要自己定义一个比较类或者函数,并将其作为模板参数传递给std::set。

在这里暂不讨论泛型lambda表达式。

6. 原理

在上文已经提到,lambda表达式的本质就是一个函数对象,即仿函数,下面分别是两个功能相同的lambda表达式和函数对象:

class Add
{
public:
    Add(int base)
            :_base(base)
    {}
    int operator()(int num)
    {
        return _base + num;
    }
private:
    int _base;
};
int main()
{
    int base = 1;

    // 函数对象
    Add add1(base);
    add1(1);

    // lambda表达式
    auto add2 = [base](int num)
    {
        return base + num;
    };
    add2(1);
    
    return 0;
}

image-20230301195139466

网站链接:https://godbolt.org/

通过汇编代码,可以看到编译器处理lambda表达式的方式和处理函数对象相同。原因是C++的lambda表达式底层实现是一个类,它重载了operator()运算符,使得它可以像函数一样被调用。这个类还包含了lambda表达式捕获的变量,它们可以是值捕获或引用捕获。

一个lambda表达式产生一个临时对象,它可以用来初始化一个栈上的变量。这个对象有构造函数和析构函数,并且遵循所有C++规则。

这就是lambda表达式即使在上层看来名字相同也不能相互赋值的原因,编译器在底层给它们各自的函数对象的类起了不同的名字。例如在VS编译器中,具体类名为:lambda_uuid

类名的 uuid (Universally Unique Identifier)是一个用于标识类的唯一标识符。

lambda_uuid即lambda表达式的类型。所以lambda表达式的“匿名”属性是对于使用者而言的,对于编译器来说是有名的。

7. 内联属性

C++的lambda表达式是一种方便的定义匿名函数对象(Go语言中的闭包)的方法,它可以在调用或作为参数传递给函数的地方直接定义。通常lambda表达式用于封装一些传递给算法或异步函数的代码。

C++11中引入了λ表达式,它可以用来定义一个**内联 (inline)**的函数,作为一个本地的对象或者一个参数。内联函数是一种编译器优化技术,它可以避免函数调用的开销,提高执行效率。λ表达式可以被编译器自动内联展开,从而减少函数调用的次数。但是,并不是所有的λ表达式都会被内联,这取决于编译器的实现和优化策略。

一般来说,lambda表达式会被内联的条件是:

  • lambda表达式作为参数传递给一个内联函数
  • lambda表达式没有被保存或者传递到其他地方
  • lambda表达式没有从更外层的函数返回

如果不满足这些条件,可以使用noinline关键字来标记lambda参数,表示不要求内联。另外,编译器也会根据实现和优化策略来决定是否内联lambda表达式。

如果一个函数被调用的频率很低,甚至只有一次,那么内联属性使得它在被调用的位置展开,能减少调用函数创建栈帧的开销。lambda表达式的内联属性是默认的,只要编译器认为它可以在某处展开,那么它就像一个内嵌语句一样。

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

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

相关文章

某智能驾驶企业:CACTER云网关为O365系统护航

01 客户背景 某智能驾驶企业是一家国际性的高科技创新型企业&#xff0c;在智能驾驶领域处于全球领先地位&#xff0c;专注于为广大客户提供个性化的智能驾驶解决方案&#xff0c;共建美好智能新时代。 使用产品&#xff1a;CACTER邮件安全云网关 02 痛点难点问题 根据Corema…

Hive---sqoop安装教程及sqoop操作

sqoop安装教程及sqoop操作 文章目录sqoop安装教程及sqoop操作上传安装包解压并更名添加jar包修改配置文件添加sqoop环境变量启动sqoop操作查看指定mysql服务器数据库中的表在hive中创建一个teacher表跟mysql的mysql50库中的teacher结构相同将mysql中mysql50库中的sc数据导出到h…

k8s学习之路 | k8s 工作负载 DaemonSet

文章目录1. DaemonSet 基础1.1 什么是 DS1.2 DS 的典型用法1.3 如何编写 DS 资源1.4 DS 示例文件1.5 DS Pod 是如何被调度的1.6 更新 DS1.7 DS 替代方案1.8 DS 工作负载字段描述2. DaemonSet 的使用2.1 每个节点运行一个2.2 DS 更新策略2.3 滚动更新2.4 OnDelete 更新2.6 更新回…

重新认识下网页水印

使用背景图图片 单独使用 css 实现&#xff0c;使用 backgroundImage&#xff0c;backgroundRepeat 将背景图片平铺到需要加水印的容器中即可。 如果希望实现旋转效果&#xff0c;可以借助伪元素&#xff0c;将背景样式放到伪元素中&#xff0c;旋转伪元素实现&#xff1a; &l…

CHAPTER 5 文件共享 - FTP

文件共享 - FTP1 FTP1.1 传输方式1. ASCII传输方式2. 二进制传输模式3. 两种传输方式的区别1.2 支持的模式1. 主动模式(PORT)2. 被动模式(PASV)3. 如何选择4. 为什么绝大部分互联网应用都是被动模式&#xff1f;1.3 搭建FTP服务器&#xff08;使用vsftpd&#xff09;1. 安装软件…

计算机中信息的表示和处理 整数和小数的二进制表示

信息的表示和处理整数进制字移位运算无符号数和有符号数加法运算小数定点表示IEEE 浮点表示规格化和非规格化舍入浮点运算现代计算机存储和处理的信息以二值信号表示&#xff0c;这些二进制数字称为位&#xff0c;为什么要用二进制来进行编码&#xff1f;因为二进制只有1和0两种…

信捷 XDH Ethercat A_STOP指令

本指令使运动中的轴进行减速停止/急停。最常见的情况是用来停止以指定速度运行的轴。只要在运动&#xff0c;都可以用本指令停止。上图中&#xff0c;在M150的上升沿&#xff0c;执行A_STOP指令。A_STOP HD150 D150 M151 K0HD150--输入参数起始地址&#xff0c;HD158--输入参数…

创宇猎幽APT流量监测系统获CSTC年度网络安全优秀案例

近日&#xff0c;由中国软件评测中心发起的“2022 年度网络安全和数据安全优秀案例评选”活动评选结果正式公布。在众多参与案例中&#xff0c;创宇猎幽APT流量监测系统&#xff08;NDR&#xff09;凭借出色的行业场景实践和出众的产品表现力脱颖而出&#xff0c;成功入选“202…

5、score diffusion model

DDIM中遇到的Score-based SDE这里采用表示神经网络的预测值&#xff0c;用表示。同时等价于DDPM回顾贝叶斯公式原始公式前向过程表示连乘反向过程由前向过程可知&#xff1a;所以&#xff1a;正态分布&#xff1a;重参数技巧&#xff1a;反向过程&#xff1a;优化目标&#xff…

ASP.NETCore学习资料

1.ASP.NETCore比ASP.NET更具优势的地方是什么&#xff1f; ASP.NET Core(ASP.NET Core 简介) ASP.NET Core 是一个跨平台的开源框架&#xff0c;用于在 Windows、macOS 或 Linux 上生成基于云的新式 Web 应用。 ASP.NET(ASP.NET 简介) ASP.NET 是一个成熟的框架&#xff0c;提…

Git学习笔记(七)——其他操作

一、自定义Git Git除了配置user.name 和user.email 还有很多可配置项。 &#xff08;1&#xff09;命令git config --global color.ui true 让Git显示颜色&#xff0c;会让命令输出看起来更醒目.Git 会适当显示不同的颜色。 $ git config --global color.ui true查看分支会有…

延迟队列docker插件

文章目录 目录 文章目录 前言 一、环境准备与使用 总结 前言 一、环境准备与使用 下载对应版本的插件 https://github.com/rabbitmq/rabbitmq-delayed-message-exchange/releases 然后在虚拟机上随便找个文件夹 cd /home/docker 先把容器运行 然后在当前文件下 将插件拷贝…

半入耳式耳机运动会不会掉、佩戴超稳固的运动耳机推荐

现在越来越多的人开始意识到运动的重要性&#xff0c;用运动给身体增加一道“防护墙”是最好的生活方式了&#xff0c;不过&#xff0c;日复一日做着几乎相同的动作&#xff0c;难免索然无味&#xff0c;所以很多人都会选择在运动时戴上耳机听歌解闷&#xff0c;这时候也有不少…

选射频线缆的困难和调试多链路匹配板子的心酸

今日痛点&#xff1a;选电缆和板子出问题 选线缆选到后面&#xff1a;有同事给我说&#xff0c;我还要高精度转接头&#xff0c;我还要BNC转接头 ​ 你们如果知道我选择线缆的艰辛&#xff0c;换做你们会怎么想 附上我选择线缆的心得&#xff1a; 1.S11尽量要考虑桌子的宽度&a…

Vector - CAPL - 文件处理函数

在当前平台化的趋势下,就算是协议层测试依然需要适配各种各样的项目,也需要处理各类型的文件,那我们如何对文件进行读取、写入、修改等类型的操作呢?今天我们就会介绍此类型的函数,主要适用于text、bin文件的处理。 打开文件 Open

MySQL之Explain分析

4 Explain分析&#xff08;重点&#xff09; 4.1 Explain介绍 使用EXPLAIN关键字可以模拟优化器执行SQL语句&#xff0c;分析你的查询语句或是结构的性能瓶颈 在 select 语句之前增加 explain 关键字&#xff0c;MySQL 会在查询上设置一个标记&#xff0c;执行查询会返回执行…

TCP的11种状态

CLOSED状态&#xff1a;初始状态&#xff0c;表示TCP连接是“关闭的”或者“未打开的”LISTEN状态&#xff1a;表示服务端的某个端口正处于监听状态&#xff0c;正在等待客户端连接的到来SYN_SENT状态&#xff1a;当客户端发送SYN请求建立连接之后&#xff0c;客户端处于SYN_SE…

Gradle安装配置阿里云

Gradle是一个基于Apache Ant和Apache Maven概念的项目自动化构建开源工具。它使用一种基于Groovy的特定领域语言(DSL)来声明项目设置&#xff0c;也增加了基于Kotlin语言的kotlin-based DSL&#xff0c;抛弃了基于XML的各种繁琐配置。 面向Java应用为主。当前其支持的语言C、J…

数据结构——哈希表

一、哈希表介绍1.1 哈希表初了解哈希表是属于一个数据结构&#xff0c;并不是一个算法哈希表&#xff1a;hashtable&#xff0c;也叫散列表&#xff0c;根据关键码值(Key value)而直接进行访问的数据结构。通过把关键码值映射到表中的一个位置来访问记录&#xff0c;以加快查找…

解析永春堂1300模式为何风靡新零售市场

最近&#xff0c;永春堂1300模式风靡新零售市场&#xff0c;它凭借兼顾大、小、新、老会员&#xff0c;没有沉淀和泡沫等特点&#xff0c;引起市场的热切关注。而永春堂1300模式如此受欢迎的原因&#xff0c;最重要的&#xff0c;还是它丰厚的奖项报酬。永春堂1300直销模式主要…