C++11之后引入了lambda表达式, 经过C++之前版本洗礼的同学们相信都会深感lamda的简便和强大吧, 那么lamda到底是个什么神奇物种呢, 我通过一个小例子再来从细胞级别来一次分析。
分析环境:Windows + Visual Studio2013
C++版本:C++11
分析原理,用代码说话,先来一段demo代码:
搞一个简单的console工程, 代码贴进去跑起来, 在int a = 0; 这一句打上断点, 程序正常断下( 这句话一出,感觉回到了xxx时代), 在宇宙第一调试器中转到ASM代码界面,于是得如下代码:
快速定位代码, 红色、绿色、橙色 我们关心的核心代码,其他部分,最上面一部分是函数堆栈防移除保护代码,直接忽略, 最下面一部分是printf调用和堆栈防溢出cookie校验,直接忽略
红色部分, 初始化 a = 0, b = 1
蓝色部分, 看到最底部的Call, 就是函数调用, 从符号名称上可以直观判断出来这是一个类的构造函数, 这个构造函数在调用之前,堆栈上压入了3个参数, a变量地址, b变量地址, 还有 func变量的地址,
不知道这个是干什么的类,于是进一步进入跟踪, 宇宙调试器F11进入,主要代码如下:
继续快速定位核心代码, 其实这段代码Visual Studio经过了符号化,看起来已经是比较坑了,不容易直观感受对象的内存布局,来一段去符号化的代码:
这下内存布局操作就很直观了, 第一段,直接就是把 ECX 值给内部的一个变量保存起来, 感觉上没有啥用处,后面再评价这个操作,
接着第二段,
第一句, EAX = [EBP-8], 标准栈帧操作, 取上一个本地变量的值,也就是 ECX, 此时 EAX = ECX, 而ECX从前面代码分析可知, ECX正是func变量的地址,也就是 EAX = &func
第二句,ECX = [EBP+8], 标准的栈帧操作, 取第一个参数, 从前面的分析可以知道,这个EBP+8 取的就是 b变量的地址, 也就是 ECX = &b
第三句,直接给你一个赋值操作, 让 *(DWORD *)&func = &b
到这一段结束, 可以的到一个小的对象布局了,
接着第三段,
第一句, EAX = [EBP-8], 还是进行 EAX = &func 操作
第二句, ECX = &a, 把上层的a变量地址拿到, 保存到ECX
第三句,EDX = [ECX], 翻译一下就是 EDX = *(DWORD *)&a, 这个动作就是读取a变量的值, 也就是 EDX = 0
第四句, 又是直接一个赋值操作,让 *(DWORD *)((char *)&func + 4) = EDX
这一段结束, 构造函数基本就走完了, 这个时候内存布局也构造完毕了,这个时候的内存布局是:
到这里再观察 我们前面遗留的评价问题, 其实就是 ECX 后面要复用了, 得把里面的东西保存起来编译恢复, 没有什么稀奇的。
所以,Visual Studio给我们编译合成的代码就可以翻译一波了,
然后继续回到main函数的执行流程, 这回就是开始调用 lamda函数体了,
进入 <lambda_b328511335c7e943aa98460a349659c7>::operator() 这个函数调用,
代码不一行行翻译了, 见上面的注释, 到这里整个流程就分析清楚了,
分析结论:整个lamda表达式,编译的时候,
1. 编译器给你自动生成一个形如 <lambda_b328511335c7e943aa98460a349659c7> 的类
2. 然后把捕获列表中的参数,都按照你的要求(值捕获, 引用捕获)包装到这个类的成员里面
3. 编译器生成一个 operator() 重载函数, 最后你对lamda的调用就是对函数对象的调用了, 捕获的参数早给你准备好了
从最细微处观察了 lamda 的编译器实现, 以后 捕获列表 里面的东西该怎么搞心里就很清楚啦,也少了纠结
错误之处,欢迎指正哈!
分析环境:Windows + Visual Studio2013
C++版本:C++11
分析原理,用代码说话,先来一段demo代码:
搞一个简单的console工程, 代码贴进去跑起来, 在int a = 0; 这一句打上断点, 程序正常断下( 这句话一出,感觉回到了xxx时代), 在宇宙第一调试器中转到ASM代码界面,于是得如下代码:
快速定位代码, 红色、绿色、橙色 我们关心的核心代码,其他部分,最上面一部分是函数堆栈防移除保护代码,直接忽略, 最下面一部分是printf调用和堆栈防溢出cookie校验,直接忽略
红色部分, 初始化 a = 0, b = 1
蓝色部分, 看到最底部的Call, 就是函数调用, 从符号名称上可以直观判断出来这是一个类的构造函数, 这个构造函数在调用之前,堆栈上压入了3个参数, a变量地址, b变量地址, 还有 func变量的地址,
不知道这个是干什么的类,于是进一步进入跟踪, 宇宙调试器F11进入,主要代码如下:
继续快速定位核心代码, 其实这段代码Visual Studio经过了符号化,看起来已经是比较坑了,不容易直观感受对象的内存布局,来一段去符号化的代码:
这下内存布局操作就很直观了, 第一段,直接就是把 ECX 值给内部的一个变量保存起来, 感觉上没有啥用处,后面再评价这个操作,
接着第二段,
第一句, EAX = [EBP-8], 标准栈帧操作, 取上一个本地变量的值,也就是 ECX, 此时 EAX = ECX, 而ECX从前面代码分析可知, ECX正是func变量的地址,也就是 EAX = &func
第二句,ECX = [EBP+8], 标准的栈帧操作, 取第一个参数, 从前面的分析可以知道,这个EBP+8 取的就是 b变量的地址, 也就是 ECX = &b
第三句,直接给你一个赋值操作, 让 *(DWORD *)&func = &b
到这一段结束, 可以的到一个小的对象布局了,
接着第三段,
第一句, EAX = [EBP-8], 还是进行 EAX = &func 操作
第二句, ECX = &a, 把上层的a变量地址拿到, 保存到ECX
第三句,EDX = [ECX], 翻译一下就是 EDX = *(DWORD *)&a, 这个动作就是读取a变量的值, 也就是 EDX = 0
第四句, 又是直接一个赋值操作,让 *(DWORD *)((char *)&func + 4) = EDX
这一段结束, 构造函数基本就走完了, 这个时候内存布局也构造完毕了,这个时候的内存布局是:
到这里再观察 我们前面遗留的评价问题, 其实就是 ECX 后面要复用了, 得把里面的东西保存起来编译恢复, 没有什么稀奇的。
所以,Visual Studio给我们编译合成的代码就可以翻译一波了,
然后继续回到main函数的执行流程, 这回就是开始调用 lamda函数体了,
进入 <lambda_b328511335c7e943aa98460a349659c7>::operator() 这个函数调用,
代码不一行行翻译了, 见上面的注释, 到这里整个流程就分析清楚了,
分析结论:整个lamda表达式,编译的时候,
1. 编译器给你自动生成一个形如 <lambda_b328511335c7e943aa98460a349659c7> 的类
2. 然后把捕获列表中的参数,都按照你的要求(值捕获, 引用捕获)包装到这个类的成员里面
3. 编译器生成一个 operator() 重载函数, 最后你对lamda的调用就是对函数对象的调用了, 捕获的参数早给你准备好了
从最细微处观察了 lamda 的编译器实现, 以后 捕获列表 里面的东西该怎么搞心里就很清楚啦,也少了纠结
错误之处,欢迎指正哈!