C++ lambda 分析
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, 允许 函数体 修改各个复制捕获的形参constexprC++ 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代码还是比较多的,由于一些隐含规则的约束下,编译器做了很多的工作,产生的代码的顺序就比较混乱
- 使用 
=值捕获时,会先调用copy构造函数 - 使用 
&引用捕获时,将捕获对象的引用(地址)作为隐式参数传给匿名函数 - 编译器不仅会产生匿名函数,还会有一个析构函数产生,这个函数负责调用在匿名函数内的析构函数
 
生命周期
lambda表达式相关的对象的生命周期,见上反汇编:
- 全局,更外层作用域的生命周期不受影响
 - 使用值捕获的情况,先于lambda表达式函数体构造对象,后于函数体执行完析构
 - 在lambda表达式函数体内的对象,在函数体执行时创建,在闭包析构函数内析构
 - lambda 对象的生命周期为所在作用域结束,析构的顺序为声明的逆序析构
 
this
使用 -std=c++14 生成的汇编代码在 =,&,this 捕获的情况下,产生的汇编代码几乎一样,都是使用的引用(this地址)传参,使用 -std=c++2a 的情况下,编译器不推荐使用值捕获的方式(虽然还是使用的引用捕获)。
TODO
- 补全对参数包的分析
 
参考
lambda 表达式,cppreference Lambda 表达式 (C++11 起)。
C++ lambda 分析的更多相关文章
- Scala - Spark Lambda“goesto“ => 分析
		
/// 定义一个函数AddNoise,参数分别为rdd,Fraction.其中rdd为(BreezeDenseMatrix, BreezeDenseMatrix)元组构成的RDD.Fraction为一 ...
 - Java 8 Lambda实现原理分析
		
PDF文档已上传Github Github:https://github.com/zwjlpeng/Angrily_Learn_Java_8 为了支持函数式编程,Java 8引入了Lambda表达式 ...
 - 你真的了解lambda吗?一文让你明白lambda用法与源码分析
		
本文作者: cmlanche 本文链接: http://www.cmlanche.com/2018/07/22/lambda用法与源码分析/ 转载来源:cmlanche.com 用法 示例:最普遍的一 ...
 - 你真的了解java的lambda吗?- java lambda用法与源码分析
		
你真的了解java的lambda吗?- java lambda用法与源码分析 转载请注明来源:cmlanche.com 用法 示例:最普遍的一个例子,执行一个线程 new Thread(() -> ...
 - java8 探讨与分析匿名内部类、lambda表达式、方法引用的底层实现
		
问题解决思路:查看编译生成的字节码文件 目录 测试匿名内部类的实现 小结 测试lambda表达式 小结 测试方法引用 小结 三种实现方式的总结 对于lambda表达式,为什么java8要这样做? 理论 ...
 - Lambda表达式底层分析
		
一.我们先看下C#代码下Lamdba表达式的写法 // <summary> /// 写入日志委托 /// </summary> /// <param name=" ...
 - 深度分析:java8的新特性lambda和stream流,看完你学会了吗?
		
1. lambda表达式 1.1 什么是lambda 以java为例,可以对一个java变量赋一个值,比如int a = 1,而对于一个方法,一块代码也是赋予给一个变量的,对于这块代码,或者说被赋给变 ...
 - Java Lambda 表达式源码分析
		
基本概念 Lambda 表达式 函数式接口 方法引用 深入实现原理 字节码 为什么不使用匿名内部类? invokedynamic 总结 参考链接 GitHub 项目 Lambda 表达式是什么?JVM ...
 - 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 ...
 
随机推荐
- Web中的通配符
			
/**的意思是所有文件夹及里面的子文件夹 /*是所有文件夹,不含子文件夹 /是web项目的根目录 http://www.coderanch.com/t/364782/Servlets/java ...
 - css隐藏滚动条、移动端滚动卡顿的解决
			
1.如果想保持容器能够滚动,同时不想看到丑陋的滚动条,chrome.firefox和移动端上不考虑兼容性直接 element::-webkit-scrollbar{ display:none } 2. ...
 - c# 写个简单的爬虫。注:就一个方法,没有注释,自己猜~哈哈
			
和我,在成都的街头走一走,哦~喔~哦~ public JsonResult GetHtml() { string url = "http://www.xxxxxxxxxxxxxxxxxx.c ...
 - 非GUI-Qt程序运行后显示Console(简单好用:在pro文件中加入: CONFIG += console)
			
----我的生活,我的点点滴滴!! 有很多时候,我们在程序中添加了好Debug信息,方便程序在运行期间打印出一些我们需要的信息或者,想用他来显示一些必要信息时, 那么console就太重要了,曾几何时 ...
 - DEVOPS技术实践_21:Pipeline的嵌套以及流程控制的if和case语句
			
1 if控制语句 使用一个简单的If控制语句 pipeline { agent any stages { stage('flow control') { steps { script { == ) { ...
 - Python基础入门必备知识
			
1 标识符标识符是编程时使用的名字,用于给变量.函数.语句块等命名,Python 中标识符由字母.数字.下划线组成,不能以数字开头,区分大小写. 以下划线开头的标识符有特殊含义,单下划线开头的标识符, ...
 - Spring Security详解
			
Spring Security 一. 简介 Spring Security是一个能够为基于Spring的企业应用系统提供声明式的安全访问控制解决方案的安全框架.它提供了一组可以在Spring应用上下文 ...
 - .NET Core 3.1和WorkerServices构建Windows服务
			
介绍 ASP.NET Core 3增加了一个非常有意思的功能Worker Service.他是一个ASP.NET Core模板,他允许我们创建托管长期的运行的后台服务,这些服务具体实现IHostedS ...
 - MATLAB实例:PCA(主成成分分析)详解
			
MATLAB实例:PCA(主成成分分析)详解 作者:凯鲁嘎吉 - 博客园 http://www.cnblogs.com/kailugaji/ 1. 主成成分分析 2. MATLAB解释 详细信息请看: ...
 - java基础之----非空判断
			
大家好,第一次写博客,一直想写博客,用于自我总结,也用于帮助新同学成长. 平常我们开发的时候,用到很多非空判断,但是很多同学用到的地方不是很准确,这里,我把自己平时遇到的坑跟大家说说.我废话不多,只想 ...