C++11 lambda表达式 原理

来源:互联网 发布:网络集成平台 编辑:程序博客网 时间:2024/06/05 21:07
        C++11之后引入了lambda表达式, 经过C++之前版本洗礼的同学们相信都会深感lamda的简便和强大吧, 那么lamda到底是个什么神奇物种呢, 我通过一个小例子再来从细胞级别来一次分析。

分析环境:Windows + Visual Studio2013
C++版本:C++11

分析原理,用代码说话,先来一段demo代码:
  1. void main()
  2. {
  3.      int a = 0;
  4.      int b = 1;
  5.      auto func = [=, &b](int c){
  6.          printf("in lamda, a=%d, b=%d, c=%d\n", a, b, c);
  7.          b = 2;
  8.      };

  9.      func(2);
  10.      printf("now, b=%d\n", b);

  11.      getchar();
  12. }
复制代码


搞一个简单的console工程, 代码贴进去跑起来, 在int a = 0; 这一句打上断点, 程序正常断下(这句话一出,感觉回到了xxx时代), 在宇宙第一调试器中转到ASM代码界面,于是得如下代码:
  1. push        ebp  
  2. mov         ebp,esp  
  3. sub         esp,0E8h  
  4. push        ebx  
  5. push        esi  
  6. push        edi  
  7. lea         edi,[ebp-0E8h]  
  8. mov         ecx,3Ah  
  9. mov         eax,0CCCCCCCCh  
  10. rep stos    dword ptr es:[edi]  
  11. mov         dword ptr [a],0  
  12. mov         dword ptr [b],1 
  13. lea         eax,[a]  
  14. push        eax  
  15. lea         ecx,[b]  
  16. push        ecx  
  17. lea         ecx,[func]  
  18. call        <lambda_b328511335c7e943aa98460a349659c7>::<lambda_b328511335c7e943aa98460a349659c7> (012513C0h) 
  19. push        2  
  20. lea         ecx,[func]  
  21. call        <lambda_b328511335c7e943aa98460a349659c7>::operator() (01251420h)  
  22. mov         esi,esp  
  23. mov         eax,dword ptr [b]  
  24. push        eax  
  25. push        1255858h  
  26. call        dword ptr ds:[1259110h]  
  27. add         esp,8  
  28. cmp         esi,esp  
  29. call        __RTC_CheckEsp (01251136h)  
  30. mov         esi,esp  
  31. call        dword ptr ds:[1259118h]  
  32. cmp         esi,esp  
  33. call        __RTC_CheckEsp (01251136h)  
  34. xor         eax,eax  
  35. push        edx  
  36. mov         ecx,ebp  
  37. push        eax  
  38. lea         edx,ds:[1251548h]  
  39. call        @_RTC_CheckStackVars@8 (01251087h)  
  40. pop         eax  
  41. pop         edx  
  42. pop         edi  
  43. pop         esi  
  44. pop         ebx  
  45. add         esp,0E8h  
  46. cmp         ebp,esp  
  47. call        __RTC_CheckEsp (01251136h)  
  48. mov         esp,ebp  
  49. pop         ebp  
  50. ret  
复制代码
快速定位代码, 红色、绿色、橙色 我们关心的核心代码,其他部分,最上面一部分是函数堆栈防移除保护代码,直接忽略, 最下面一部分是printf调用和堆栈防溢出cookie校验,直接忽略

红色部分, 初始化 a = 0, b = 1
蓝色部分, 看到最底部的Call, 就是函数调用, 从符号名称上可以直观判断出来这是一个类的构造函数, 这个构造函数在调用之前,堆栈上压入了3个参数, a变量地址, b变量地址, 还有 func变量的地址,
不知道这个是干什么的类,于是进一步进入跟踪, 宇宙调试器F11进入,主要代码如下:
  1. push        ebp  
  2. mov         ebp,esp  
  3. sub         esp,0CCh  
  4. push        ebx  
  5. push        esi  
  6. push        edi  
  7. push        ecx  
  8. lea         edi,[ebp-0CCh]  
  9. mov         ecx,33h  
  10. mov         eax,0CCCCCCCCh  
  11. rep stos    dword ptr es:[edi]  
  12. pop         ecx  
  13. mov         dword ptr [this],ecx
  14. mov         eax,dword ptr [this]
  15. mov         ecx,dword ptr [__b]
  16. mov         dword ptr [eax],ecx
  17. mov         eax,dword ptr [this]
  18. mov         ecx,dword ptr [__a]
  19. mov         edx,dword ptr [ecx]
  20. mov         dword ptr [eax+4],edx
  21. mov         eax,dword ptr [this]
  22. pop         edi  
  23. pop         esi  
  24. pop         ebx  
  25. mov         esp,ebp  
  26. pop         ebp  
  27. ret         8  
复制代码
继续快速定位核心代码, 其实这段代码Visual Studio经过了符号化,看起来已经是比较坑了,不容易直观感受对象的内存布局,来一段去符号化的代码:

  1. MOV     DWORD PTR SS:[EBP-8],ECX
复制代码
这下内存布局操作就很直观了, 第一段,直接就是把 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给我们编译合成的代码就可以翻译一波了,
  1. <lambda_b328511335c7e943aa98460a349659c7>::<lambda_b328511335c7e943aa98460a349659c7>
  2. :m_var_a(a), m_var_b(&b)
  3. {
  4. }
复制代码



然后继续回到main函数的执行流程, 这回就是开始调用 lamda函数体了, 
  1. push 2 
  2. lea ecx,[func] 
  3. call <lambda_b328511335c7e943aa98460a349659c7>::operator() (01251420h)
复制代码
进入 <lambda_b328511335c7e943aa98460a349659c7>::operator() 这个函数调用,
  1. push        edi  
  2. push        ecx  
  3. lea         edi,[ebp-0CCh]  
  4. mov         ecx,33h  
  5. mov         eax,0CCCCCCCCh  
  6. rep stos    dword ptr es:[edi]  
  7. pop         ecx  
  8. mov         dword ptr [this],ecx  
  9. mov         esi,esp  
  10. mov         eax,dword ptr [c]  
  11. push        eax  
  12. mov         ecx,dword ptr [this]  
  13. mov         edx,dword ptr [ecx]  
  14. mov         eax,dword ptr [edx]  
  15. push        eax  
  16. mov         ecx,dword ptr [this]  
  17. mov         edx,dword ptr [ecx+4]  
  18. push        edx  
  19. push        12559E4h  
  20. call        dword ptr ds:[1259110h] -------------------->printf("in lamda, a=%d, b=%d, c=%d\n", a, b, c);
  21. add         esp,10h  
  22. cmp         esi,esp  
  23. call        __RTC_CheckEsp (01251136h)  
  24. mov         eax,dword ptr [this]  
  25. mov         ecx,dword ptr [eax]  
  26. mov         dword ptr [ecx],2          -------------------->b = 2
  27. pop         edi  
  28. pop         esi  
  29. pop         ebx  
  30. add         esp,0CCh  
  31. cmp         ebp,esp  
  32. call        __RTC_CheckEsp (01251136h)  
  33. mov         esp,ebp  
  34. pop         ebp  
  35. ret         4
复制代码

代码不一行行翻译了, 见上面的注释, 到这里整个流程就分析清楚了, 

分析结论:整个lamda表达式,编译的时候,
1. 编译器给你自动生成一个形如 <lambda_b328511335c7e943aa98460a349659c7> 的类
2. 然后把捕获列表中的参数,都按照你的要求(值捕获, 引用捕获)包装到这个类的成员里面
3. 编译器生成一个 operator() 重载函数, 最后你对lamda的调用就是对函数对象的调用了, 捕获的参数早给你准备好了

从最细微处观察了 lamda 的编译器实现, 以后 捕获列表 里面的东西该怎么搞心里就很清楚啦,也少了纠结

错误之处,欢迎指正哈!
原创粉丝点击