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. Web中的通配符

    /**的意思是所有文件夹及里面的子文件夹 /*是所有文件夹,不含子文件夹 /是web项目的根目录     http://www.coderanch.com/t/364782/Servlets/java ...

  2. css隐藏滚动条、移动端滚动卡顿的解决

    1.如果想保持容器能够滚动,同时不想看到丑陋的滚动条,chrome.firefox和移动端上不考虑兼容性直接 element::-webkit-scrollbar{ display:none } 2. ...

  3. c# 写个简单的爬虫。注:就一个方法,没有注释,自己猜~哈哈

    和我,在成都的街头走一走,哦~喔~哦~ public JsonResult GetHtml() { string url = "http://www.xxxxxxxxxxxxxxxxxx.c ...

  4. 非GUI-Qt程序运行后显示Console(简单好用:在pro文件中加入: CONFIG += console)

    ----我的生活,我的点点滴滴!! 有很多时候,我们在程序中添加了好Debug信息,方便程序在运行期间打印出一些我们需要的信息或者,想用他来显示一些必要信息时, 那么console就太重要了,曾几何时 ...

  5. DEVOPS技术实践_21:Pipeline的嵌套以及流程控制的if和case语句

    1 if控制语句 使用一个简单的If控制语句 pipeline { agent any stages { stage('flow control') { steps { script { == ) { ...

  6. Python基础入门必备知识

    1 标识符标识符是编程时使用的名字,用于给变量.函数.语句块等命名,Python 中标识符由字母.数字.下划线组成,不能以数字开头,区分大小写. 以下划线开头的标识符有特殊含义,单下划线开头的标识符, ...

  7. Spring Security详解

    Spring Security 一. 简介 Spring Security是一个能够为基于Spring的企业应用系统提供声明式的安全访问控制解决方案的安全框架.它提供了一组可以在Spring应用上下文 ...

  8. .NET Core 3.1和WorkerServices构建Windows服务

    介绍 ASP.NET Core 3增加了一个非常有意思的功能Worker Service.他是一个ASP.NET Core模板,他允许我们创建托管长期的运行的后台服务,这些服务具体实现IHostedS ...

  9. MATLAB实例:PCA(主成成分分析)详解

    MATLAB实例:PCA(主成成分分析)详解 作者:凯鲁嘎吉 - 博客园 http://www.cnblogs.com/kailugaji/ 1. 主成成分分析 2. MATLAB解释 详细信息请看: ...

  10. java基础之----非空判断

    大家好,第一次写博客,一直想写博客,用于自我总结,也用于帮助新同学成长. 平常我们开发的时候,用到很多非空判断,但是很多同学用到的地方不是很准确,这里,我把自己平时遇到的坑跟大家说说.我废话不多,只想 ...