lambda 表达式分析

构造闭包:能够捕获作用域中变量的匿名函数的对象,Lambda 表达式是纯右值表达式,其类型是独有的无名非联合非聚合类类型,被称为闭包类型(closure type),所以在声明的时候必须使用 auto 来声明。

在其它语言如lua中,闭包的格式相对更为简单,可以使用 lambda 表达式作用域的所有变量,并且返回闭包

local function add10(arg)
local i = 10
local ret = function()
i = i - 1
return i + arg
end
return ret
end print( add10(1)() ) -- 10

C++ 中则显得复杂些,也提供了更多的功能来控制闭包函数的属性。

lambda 和 std::function

虽然 lambda 的使用和函数对象的调用方式有相似之处,

std::function<int(int, int)> add2 = [&](int a, int b) -> int {
return a + b + val + f1.value;
};

但他们并不是同一种东西,lambda 的类型是不可知的(在编译期决定),使用 sizeof 两者的大小也是不相同的,std::function 是函数对象,通过消除类型再重载 operator() 达到调用的效果,只要这个函数满足可以调用的条件,就可以使用std::function保存起来,这也是上面例子的体现。

语法 C++ 17

  • [ 捕获 ] ( 形参 ) 说明符(可选) 异常说明 -> ret { 函数体 }

    • 全量声明
  • [ 捕获 ] ( 形参 ) -> ret { 函数体 }
    • const lambda 声明,复制捕获 的对象在 lambda 体内为 const
  • [ 捕获 ] ( 形参 ) { 函数体 }
    • 省略返回类型的声明,返回的类型从函数体的返回推导
  • [ 捕获 ] { 函数体 }
    • 无实参的函数

说明符

  • mutable, 允许 函数体 修改各个复制捕获的形参
  • constexpr C++ 17, 显式指定函数调用符为 constexpr,当函数体满足 constexpr函数要求时,即使未显式指定,也会是 constexpr

异常说明 :提供 throw 或者 noexpect 字句

使用如下:

struct Foo {
int value;
Foo() : value(1) { std::cout << "Foo::Foo();\n"; }
Foo(const Foo &other) {
value = other.value;
std::cout << "Foo::Foo(const Foo &)\n";
}
~Foo() {
value = 0;
std::cout << "Foo::~Foo();\n";
}
}; int main() {
int val = 7;
Foo f1;
auto add1 = [&](int a, int b) mutable noexcept->int {
return a + b + val + f1.value;
}; // 使用 std::function 包装
std::function<int(int, int)> add2 = [&](int a, int b) -> int {
f1.value = val; // OK,引用捕获
return a + b + val + f1.value;
};
auto add3 = [&](int a, int b) { return a + b + val + f1.value; };
auto add4 = [=] {
// f1.value = val; // 错误,复制捕获 的对象在 lambda 体内为 const
return val + f1.value;
}; // 全 auto 也是可以,返回的这个 auto 不写也行
auto add5 = [=](auto a, int b) -> auto { return a + b; };
} // 输出:
Foo::Foo();
Foo::Foo(const Foo &)
Foo::~Foo();
Foo::~Foo();

Lambda 捕获

  • &(以引用隐式捕获被使用的自动变量)
  • =(以复制隐式捕获被使用的自动变量)

当出现任一默认捕获符时,都能隐式捕获当前对象(this)。当它被隐式捕获时,始终被以引用捕获,即使默认捕获符是 = 也是如此。~~当默认捕获符为 = 时,(this) 的隐式捕获被弃用。 (C++20 起)~~,见this分析

捕获 中单独的捕获符的语法是

  • 标识符

    • 简单以复制捕获
  • 标识符 ...
    • 作为包展开的简单以复制捕获
  • 标识符 初始化器
    • 带初始化器的以复制捕获
  • & 标识符
    • 简单以引用捕获
  • & 标识符 ...
    • 作为包展开的简单引用捕获
  • & 标识符 初始化器
    • 带初始化器的以引用捕获
  • this
    • 当前对象的简单以引用捕获
  • *this
    • 当前对象的简单以复制捕获, C++17

捕获列表可以不同的捕获方式,当默认捕获符是 & 时,后继的简单捕获符必须不以 & 开始, 当默认捕获符是 = 时,后继的简单捕获符必须以 & 开始,或者为 *this (C++17 起) 或 this (C++20 起).

在上面的示例main中增加,部分代码如下,包括了两种捕获方式,及在函数体内修改lambda捕获变量的值,及返回对象

    Foo f1;
Foo f2;
int val = 7;
auto add6 = [=, &f2](int a) mutable {
f2.value *= a;
f1.value += f2.value + val;
return f1;
}; Foo f3 = add6(3);

又到了喜闻乐见反汇编的情况了,看看编译器是怎么实现的lambda表达式的。

_ZZ4mainENUliE_clEi:
.LFB10:
.cfi_startproc
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
subq $32, %rsp
movq %rdi, -8(%rbp)
movq %rsi, -16(%rbp)
movl %edx, -20(%rbp) // int a
movq -16(%rbp), %rax // -16(%rbp) = & this(f2),每次都这么赋值,没优化的指令真的很冗余
movq (%rax), %rax
movl (%rax), %edx // %edx = f2.value
movq -16(%rbp), %rax
movq (%rax), %rax
imull -20(%rbp), %edx // %edx = f2.value * a
movl %edx, (%rax) // f2.value = %edx
movq -16(%rbp), %rax
movl 8(%rax), %edx // 在main函数中 -32(%rbp) + 8 = -24(%rbp) 也就是copy构造函数产生的 this 指针
movq -16(%rbp), %rax // 以下的就是那些加减了,
movq (%rax), %rax
movl (%rax), %ecx
movq -16(%rbp), %rax
movl 12(%rax), %eax
addl %ecx, %eax
addl %eax, %edx
movq -16(%rbp), %rax
movl %edx, 8(%rax)
movq -16(%rbp), %rax
leaq 8(%rax), %rdx
movq -8(%rbp), %rax
movq %rdx, %rsi // 上一个copy构造函数内的 this 指针
movq %rax, %rdi // copy构造的this指针
call _ZN3FooC1ERKS_ // 继续调用copy构造函数,返回
movq -8(%rbp), %rax
leave
.cfi_def_cfa 7, 8
ret
.cfi_endproc // lambda 的析构函数,这个函数是隐式声明的
_ZZ4mainENUliE_D2Ev:
.LFB12:
.cfi_startproc
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
subq $16, %rsp
movq %rdi, -8(%rbp)
movq -8(%rbp), %rax
addq $8, %rax
movq %rax, %rdi
call _ZN3FooD1Ev
nop
leave
.cfi_def_cfa 7, 8
ret
.cfi_endproc main:
.LFB9:
.cfi_startproc
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
subq $48, %rsp
movl $7, -4(%rbp) // int val = 7;
leaq -8(%rbp), %rax // -8(%rbp) = this(f1)
movq %rax, %rdi
call _ZN3FooC1Ev // Foo f1;
leaq -12(%rbp), %rax // -12(%rbp) = this(f2)
movq %rax, %rdi
call _ZN3FooC1Ev // Foo f2;
leaq -12(%rbp), %rax
movq %rax, -32(%rbp) // -32(%rbp) = this(f2)
leaq -8(%rbp), %rax // 取 this(f1)
leaq -32(%rbp), %rdx
addq $8, %rdx // copy 构造函数的 this = -24(%rbp),记住这个 24
movq %rax, %rsi // 第二个参数 this(f1)
movq %rdx, %rdi // 第一个参数,调用copy构造函数的 this
call _ZN3FooC1ERKS_ // Foo(const Foo &);
movl -4(%rbp), %eax
movl %eax, -20(%rbp) // -20(%rbp) = 7
leaq -36(%rbp), %rax
leaq -32(%rbp), %rcx
movl $3, %edx
movq %rcx, %rsi // 第二个参数 this(f2) 的地址(两次 leaq)
movq %rax, %rdi // 需要返回的 Foo 对象的 this 指针
call _ZZ4mainENUliE_clEi // lambda 的匿名函数
leaq -36(%rbp), %rax
movq %rax, %rdi
call _ZN3FooD1Ev
leaq -32(%rbp), %rax
movq %rax, %rdi
call _ZZ4mainENUliE_D1Ev // 析构函数
leaq -12(%rbp), %rax
movq %rax, %rdi
call _ZN3FooD1Ev
leaq -8(%rbp), %rax
movq %rax, %rdi
call _ZN3FooD1Ev
movl $0, %eax
leave
.cfi_def_cfa 7, 8
ret
.cfi_endproc

上面的汇编代码相对cpp代码还是比较多的,由于一些隐含规则的约束下,编译器做了很多的工作,产生的代码的顺序就比较混乱

  1. 使用 = 值捕获时,会先调用copy构造函数
  2. 使用 & 引用捕获时,将捕获对象的引用(地址)作为隐式参数传给匿名函数
  3. 编译器不仅会产生匿名函数,还会有一个析构函数产生,这个函数负责调用在匿名函数内的析构函数

生命周期

lambda表达式相关的对象的生命周期,见上反汇编:

  1. 全局,更外层作用域的生命周期不受影响
  2. 使用值捕获的情况,先于lambda表达式函数体构造对象,后于函数体执行完析构
  3. 在lambda表达式函数体内的对象,在函数体执行时创建,在闭包析构函数内析构
  4. lambda 对象的生命周期为所在作用域结束,析构的顺序为声明的逆序析构

this

使用 -std=c++14 生成的汇编代码在 =&this 捕获的情况下,产生的汇编代码几乎一样,都是使用的引用(this地址)传参,使用 -std=c++2a 的情况下,编译器不推荐使用值捕获的方式(虽然还是使用的引用捕获)。

TODO

  1. 补全对参数包的分析

参考

lambda 表达式,cppreference Lambda 表达式 (C++11 起)。

C++ lambda 分析的更多相关文章

  1. Scala - Spark Lambda“goesto“ => 分析

    /// 定义一个函数AddNoise,参数分别为rdd,Fraction.其中rdd为(BreezeDenseMatrix, BreezeDenseMatrix)元组构成的RDD.Fraction为一 ...

  2. Java 8 Lambda实现原理分析

    PDF文档已上传Github  Github:https://github.com/zwjlpeng/Angrily_Learn_Java_8 为了支持函数式编程,Java 8引入了Lambda表达式 ...

  3. 你真的了解lambda吗?一文让你明白lambda用法与源码分析

    本文作者: cmlanche 本文链接: http://www.cmlanche.com/2018/07/22/lambda用法与源码分析/ 转载来源:cmlanche.com 用法 示例:最普遍的一 ...

  4. 你真的了解java的lambda吗?- java lambda用法与源码分析

    你真的了解java的lambda吗?- java lambda用法与源码分析 转载请注明来源:cmlanche.com 用法 示例:最普遍的一个例子,执行一个线程 new Thread(() -> ...

  5. java8 探讨与分析匿名内部类、lambda表达式、方法引用的底层实现

    问题解决思路:查看编译生成的字节码文件 目录 测试匿名内部类的实现 小结 测试lambda表达式 小结 测试方法引用 小结 三种实现方式的总结 对于lambda表达式,为什么java8要这样做? 理论 ...

  6. Lambda表达式底层分析

    一.我们先看下C#代码下Lamdba表达式的写法 // <summary> /// 写入日志委托 /// </summary> /// <param name=" ...

  7. 深度分析:java8的新特性lambda和stream流,看完你学会了吗?

    1. lambda表达式 1.1 什么是lambda 以java为例,可以对一个java变量赋一个值,比如int a = 1,而对于一个方法,一块代码也是赋予给一个变量的,对于这块代码,或者说被赋给变 ...

  8. Java Lambda 表达式源码分析

    基本概念 Lambda 表达式 函数式接口 方法引用 深入实现原理 字节码 为什么不使用匿名内部类? invokedynamic 总结 参考链接 GitHub 项目 Lambda 表达式是什么?JVM ...

  9. spark学习之Lambda架构日志分析流水线

    单机运行 一.环境准备 Flume 1.6.0 Hadoop 2.6.0 Spark 1.6.0 Java version 1.8.0_73 Kafka 2.11-0.9.0.1 zookeeper ...

随机推荐

  1. jQuery 工具类函数-URL操作函数

    调用名为$. param的工具函数,能使对象或数组按照key/value格式进行序列化编码,该编码后的值常用于向服务端发送URL请求,调用格式为: $. param (obj); 参数obj表示需要进 ...

  2. es6笔记 day2---字符串模板及字符串新增

    字符串连接案例 注意:引号变了,为键盘数字1旁边的飘花键 以前的老写法是在字符串中加入“+”号,给几个字符串给串起来,那种写法是要死人的. 现在只需加上一对``即可将字符串连接起来 --------- ...

  3. CodeForces 375D Tree and Queries

    传送门:https://codeforces.com/problemset/problem/375/D 题意: 给你一颗有根树,树上每个节点都有其对应的颜色,有m次询问,每次问你以点v为父节点的子树内 ...

  4. 本地项目推送到coding

      当我们本地新建了一个项目,需要放到coding上维护时,按照下面步骤即可做到. 1.先在coding上新建一个项目,并完成初始化. 2.进入到本地项目的目录下 //初始化本地仓库 a:git in ...

  5. mysql:数据库与实例的区别

    题记:最近想更深入的了解mysql,所以买了一些书在学习,趁着这个机会开个坑,整理一下一些我认为重要的知识点. 刚工作那会经常能听到组长提到实例这个词,一开始我以为是服务器... 数据库(databa ...

  6. Linux三剑客之sed的基本用法介绍

    [介绍] sed是一款强大的非交互式的文本编辑器,可以对文件文本进行增删改查的相关操作,本文主要是讲解以下sed的基本用法. [常用选项] -e 下一个参数为一个sed指令,一般只会用于同一行有多个s ...

  7. 听说你的 IDEA 昨天掉链子了?松哥给你准备了大招

    昨天一直在忙,中午抽空瞅了一眼技术群,天呐,竟然都在切磋 IDEA 激活码的事情,瞬间明白可能 jetbrains 又在搞事情了. 我大概了解了下,这次出事的主要是 2019 版,之前的 2018 版 ...

  8. 浅谈JSON HiJacking攻击

    JSON HiJacking攻击: JSON劫持类似于CSRF攻击,为了了解这种攻击方式,我们先看一下Web开发中一种常用的跨域获取数据的方式:JSONP. 先说一下JSON吧,JSON是一种数据格式 ...

  9. nodejs监听服务端口并且代理请求与静态资源

    var express = require('express'); const proxy = require('http-proxy-middleware'); const app = expres ...

  10. Clover软件使用中遇到的问题

    安装Clover应用后不生效,Win + E 时仍然默认打开系统的资源管理器 解决: 进入ie浏览器的管理加载项窗口,显示栏选择所有加载项 找到 Explorer Watcher Class 项,确保 ...