C++11新特性中的匿名函数Lambda表达式的汇编实现分析(一)

Constructs a closure: an unnamed function object capable of capturing variables in scope.

—— Lambda functions (since C++11) [cppreference.com]

按照C++11标准的说法,lambda表达式的标准格式如下:

[ capture ] ( params ) mutable exception attribute -> ret { body }
// (1) 完整的声明

[ capture ] ( params ) -> ret { body }
//(2) 一个常lambda的声明:按副本捕获的对象不能被修改。

[ capture ] ( params ) { body }
// (3) 省略后缀返回值类型:闭包的operator()的返回值类型是根据以下规则推导出的:如果body仅包含单一的return语句,那么返回值类型是返回表达式的类型(在此隐式转换之后的类型:右值到左值、数组与指针、函数到指针)否则,返回类型是void

[ capture ] { body }
//(4) 省略参数列表:函数没有参数,即参数列表是()
    capture  -  指定哪些在函数声明处的作用域中可见的符号将在函数体内可见
。```  

    符号表可按如下规则传入:

    [a,&b],按值捕获a,并按引用捕获b

    [this],按值捕获了this指针

    [&] 按引用捕获在lambda表达式所在函数的函数体中提及的全部自动储存持续性变量

    [=] 按值捕获在lambda表达式所在函数的函数体中提及的全部自动储存持续性变量

    [] 什么也没有捕获

    params  -  参数列表,与命名函数一样

    ret  -  返回值类型。如果不存在,它由该函数的return语句来隐式决定(或者是void,例如当它不返回任何值的时候)

    body  -  函数体

下面,我将从最简单的形式开始逐步对各种形式的lambda表达式进行汇编分析。

首先是最简单的类型(4):

和普通表达式一样,若单纯的一个表达式将被编译器忽略,这里将lambda表达式赋值给一个栈变量进行分析。

```javascript
int main()
{
    auto lambda = []{ };

    return 0;
}

IntelliSense显示这里的lambda变量其实是一个 void lambda(),编译后被解析是main::__l3::void(void)类型,debug查看汇编代码,发现本句并没有在main函数里产生任何汇编代码,但并不代表这个表达式没有意义,

...省略...
    auto lambda = []{ };

    return 0;
        xor         eax,eax
}
...省略...

若使用sizeof(lambda)计算其所占字节数将得到1,稍微在main代码上面一点,可以发现[]{}是作为一个函数被编译:

push        ebp
 mov         ebp,esp
 sub         esp,0CCh
 push        ebx
 push        esi
 push        edi
 push        ecx
 lea         edi,[ebp-0CCh]
 mov         ecx,33h
 mov         eax,0CCCCCCCCh
 rep stos    dword ptr es:[edi]  

 pop         ecx
 mov         dword ptr [this],ecx
 pop         edi
 pop         esi
 pop         ebx
 mov         esp,ebp
 pop         ebp
 ret
 int         3
 int         3

可见,就像普通函数一样,[]{}表达式内部被编译为一个函数,该函数内有一个this指针作为栈变量,它指向调用函数时的寄存器ecx。

下面我们执行这个lambda表达式,进入闭包内部分析,同时,为了好说明,在函数内增加一条赋值语句。

int main()
{
    auto lambda = []{
        int s = 0xA;
    };
    lambda();
    return 0;
}
对应有汇编代码:

auto lambda = []{
        int s = 0xA;
    };
    lambda();
 lea         ecx,[ebp-5]
 call        001E1570
    return 0;

可以看到,有一个地址传送,[ebp-5]的地址送给ecx,然后直接调用闭包函数。

[ebp-5]是main的一个栈变量,占用4字节,他的值没有被初始化,debug版本默认是(0xcccccccc)。

将其地址&[ebp-5]送入ecx究竟有什么含义,不妨先进入闭包函数内部看看:

push        ebp
 mov         ebp,esp
 sub         esp,0D8h
 push        ebx
 push        esi
 push        edi
 push        ecx
 lea         edi,[ebp+FFFFFF28h]
 mov         ecx,36h
 mov         eax,0CCCCCCCCh
 rep stos    dword ptr es:[edi]
 pop         ecx
 mov         dword ptr [ebp-8],ecx
        int s = 0xA;
 mov         dword ptr [ebp-14h],0Ah
    };
 pop         edi
 pop         esi
 pop         ebx
 mov         esp,ebp
 pop         ebp
 ret

可见,刚才的ecx被push保存,然后又在函数初始化栈完成后(rep stos后),被弹出并写入局部变量[ebp-8]中,而这个[ebp-8]其实就是上面说到的this指针。也就是说,这个this指针指向main中的一个局部变量。

那么,为了进一步研究这个机制,我们设法让这个闭包使用this。不妨猜想一下,this既然是指向main里面的变量,那么他可能是一个base address用来“捕获”(lambda中的概念)闭包外层作用域内的某些变量。“捕获”方式在上面有说到,若将上面的[]改为[=],让lambda按值捕获main中的int变量s,再看看有什么变化:

int main()
{
    int a = 0xB;
    auto lambda = [=]{
        int s = a;
    };
    lambda();
    return 0;
}

闭包内对应汇编代码:

pop         ecx
 mov         dword ptr [ebp-8],ecx
        int s = a;
 mov         eax,dword ptr [ebp-8]
 mov         ecx,dword ptr [eax]
 mov         dword ptr [ebp-14h],ecx
    };

同样的,先放置this指针,然后下面比较关键:

1.把this临时放到eax

2.然后再取eax地址对应的值放到临时ecx寄存器中,这里就是a

3.然后赋值给[ebp-14h]就是s

那么绕了半天做了什么事,其实就是相当于下面的代码:

那么这个this确实是指向了main里面的a,如何办到的?

查看main栈内存发现,传给闭包的this是指向下图中选中部分,而红框中是变量a:

可见,a在main的栈空间被复制了一次,而不是闭包的栈空间,那么复制发生在哪个时候,为什么this恰好就指向了a的副本?

再调用闭包函数之前,还做了一些事情:

int a = 0xB;
 mov         dword ptr [ebp-8],0Bh
    auto lambda = [=]{
        int s = a;
    };
 lea         eax,[ebp-8]
 push        eax
 lea         ecx,[ebp-14h]
 call        010E1BE0
    lambda();
 lea         ecx,[ebp-14h]
 call        010E1C20
    return 0;

发现还call了一个带参函数:

1.将a的地址送入eax并压栈,相当于给下面的函数传参&a

2.将给后面闭包用的this保存在ecx中,可能会给下面的一个call使用

上面的操作相当于下面的伪代码:

call 010E1BE0( &a , this); //当然,this并不是作为参数传入的,这里只是方便理解

可以预见,010E1BE0函数的作用应该是拷贝a,并让this指向a,空口无凭,进去看看:

push        ebp
 mov         ebp,esp
 sub         esp,0CCh
 push        ebx
 push        esi
 push        edi
 push        ecx
 lea         edi,[ebp+FFFFFF34h]
 mov         ecx,33h
 mov         eax,0CCCCCCCCh
 rep stos    dword ptr es:[edi]  

 pop         ecx
 mov         dword ptr [ebp-8],ecx
 mov         eax,dword ptr [ebp-8]
 mov         ecx,dword ptr [ebp+8]
 mov         edx,dword ptr [ecx]
 mov         dword ptr [eax],edx
 mov         eax,dword ptr [ebp-8]  

 pop         edi
 pop         esi
 pop         ebx
 mov         esp,ebp
 pop         ebp
 ret         4

前后的代码按部就班,主要是中间:

1.ecx是this不用说了。

2.先把this保存到该函数的栈空间再说

3.this放进eax,预见下面的[eax]就是*this,和上面说到的一样

4.然后是[ebp+8]这块,送给ecx临时保存,然后取值,送入edx临时保存,可见[ebp+8]里面应该是一个地址

5.edx送给*this

6.最后那个mov eax,[ebp-8] ,又把this作为返回值

关于[ebp+8]:还记得传入该函数的参数&a吗?没错,[ebp+8]保存的是就是&a。

简单翻译一下这个函数的意思:

fun(&a,this);

int fun(int in,int* this)

{

    this = in;

    return this;

}

注意这里的this传递其实是通过寄存器的方式。

好了,说了半天,刚才那个问题,差不多也知道答案了。

调用闭包函数前,“捕获者”this指针被放在main中,并对其指向的内存块拷贝闭包中要用到的变量值,调用时,this通过寄存器送入闭包中,闭包通过this访问外层作用域(这里是main)的已捕获对象(这里是a)。

可见,如果闭包要按值捕获main中多个变量,那么事先要调用一个复制函数,依次复制所有要用的变量,然后通过this寻址访问main中变量的副本,而不是把所有变量拷贝到闭包的栈空间内。

时间: 2024-11-03 08:47:48

C++11新特性中的匿名函数Lambda表达式的汇编实现分析(一)的相关文章

C++11新特性中的匿名函数Lambda表达式的汇编实现分析(三)

Lambda表达式中较复杂的形式如下: [ capture ] ( params ) -> ret { body } 现在我们构造一个简单的Lambda闭包函数进行分析: int main() { int c = 10; auto lambda = [&] (int a, int b)->int{ return a + b - c; }; int r = lambda(1, 2); return 0; } 上面的代码中,lambda表达式要求传递两个参数a和b,并按引用捕获c,计算后的

C++11新特性中的匿名函数Lambda表达式的汇编实现分析(二)

首先,让我们来看看以&方式进行变量捕获,同样没有参数和返回. int main() { int a = 0xB; auto lambda = [&]{ a = 0xA; }; lambda(); return 0; } 闭包中将main中a变量改写为0xA. main中的关键汇编代码: int a = 0xB; mov dword ptr [ebp-8],0Bh auto lambda = [&]{ a = 0xA; }; lea eax,[ebp-8] push eax lea

.net framework3.5新特性(2):Lambda表达式

 本文为原创,如需转载,请注明作者和出处,谢谢!    在C#2.0及C#1.x中,需要使用delegate来定义方法指针.如下面的代码如示: public delegate bool Filter(int num);  // delegate类型 public int[] searchArray(int[] values, Filter filter) {     List<int> result = new List<int>();    foreach (int i in v

C++11新特性:Lambda函数(匿名函数)

声明:本文参考了Alex Allain的文章http://www.cprogramming.com/c++11/c++11-lambda-closures.html 加入了自己的理解,不是简单的翻译   C++11终于知道要在语言中加入匿名函数了.匿名函数在很多时候可以为编码提供便利,这在下文会提到.很多语言中的匿名函数,如C++,都是用Lambda表达式实现的.Lambda表达式又称为lambda函数.我在下文中称之为Lambda函数. 为了明白Lambda函数的用处,请务必先搞明白C++中的

结合C++11新特性来学习C++中lambda表达式的用法_C 语言

在 C++ 11 中,lambda 表达式(通常称为 "lambda")是一种在被调用的位置或作为参数传递给函数的位置定义匿名函数对象的简便方法. Lambda 通常用于封装传递给算法或异步方法的少量代码行. 本文定义了 lambda 是什么,将 lambda 与其他编程技术进行比较,描述其优点,并提供一个基本示例.Lambda 表达式的各部分ISO C++ 标准展示了作为第三个参数传递给 std::sort() 函数的简单 lambda: #include <algorithm

C++ 11 新特性

C++ 11  新特性 类内成员赋初值 类内数据成员允许赋默认值. C11以前是会报错的.ISO C++ forbids initialization of member `name_var_' lambda表达式 lambda表达式本质上是一个未命名的内联函数. 很多语言都提供了 lambda 表达式,如 Python,Java 8.lambda 表达式可以方便地构造匿名函数,如果你的代码里面存在大量的小函数,而这些函数一般只被调用一次,那么不妨将他们重构成 lambda 表达式,简化编程.l

浅析C++11新特性的Lambda表达式_C 语言

lambda简介 熟悉Python的程序员应该对lambda不陌生.简单来说,lambda就是一个匿名的可调用代码块.在C++11新标准中,lambda具有如下格式: [capture list] (parameter list) -> return type { function body } 可以看到,他有四个组成部分:     1.capture list: 捕获列表     2.parameter list: 参数列表     3.return type: 返回类型     4.func

js中匿名函数的创建与调用方法分析

 匿名函数就是没有名字的函数了,也叫闭包函数(closures),允许 临时创建一个没有指定名称的函数.最经常用作回调函数(callback)参数的值,很多新手朋友对于匿名函数不了解.这里就来分析一下. function 函数名(参数列表){函数体;} 如果是创建匿名函数,那就应该是: function(){函数体;} 因为是匿名函数,所以一般也不会有参数传给他. 为什么要创建匿名函数呢?在什么情况下会使用到匿名函数.匿名函数主要有两种常用的场景,一是回调函数,二是直接执行函数. 回调函数,像a

js中匿名函数的创建与调用方法分析_javascript技巧

本文实例分析了js中匿名函数的创建与调用方法.分享给大家供大家参考.具体实现方法如下: 匿名函数就是没有名字的函数了,也叫闭包函数(closures),允许 临时创建一个没有指定名称的函数.最经常用作回调函数(callback)参数的值,很多新手朋友对于匿名函数不了解.这里就来分析一下. function 函数名(参数列表){函数体;} 如果是创建匿名函数,那就应该是: function(){函数体;} 因为是匿名函数,所以一般也不会有参数传给他. 为什么要创建匿名函数呢?在什么情况下会使用到匿