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表达式赋值给一个栈变量进行分析。

int main()
{

auto lambda = []{ };

return 0;

}

IntelliSense显示这里的lambda变量其实是一个 void lambda(),编译后被解析是main::__l3::void<lambda>(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

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

![image](https://yqfile.alicdn.com/b5627b467f9a63f16227789879196e389c9af88c.png)


那么这个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)。
相关文章
|
程序员 编译器 C++
【实战指南】C++ lambda表达式使用总结
Lambda表达式是C++11引入的特性,简洁灵活,可作为匿名函数使用,支持捕获变量,提升代码可读性与开发效率。本文详解其基本用法与捕获机制。
435 88
|
编译器 C++ 容器
【c++11】c++11新特性(上)(列表初始化、右值引用和移动语义、类的新默认成员函数、lambda表达式)
C++11为C++带来了革命性变化,引入了列表初始化、右值引用、移动语义、类的新默认成员函数和lambda表达式等特性。列表初始化统一了对象初始化方式,initializer_list简化了容器多元素初始化;右值引用和移动语义优化了资源管理,减少拷贝开销;类新增移动构造和移动赋值函数提升性能;lambda表达式提供匿名函数对象,增强代码简洁性和灵活性。这些特性共同推动了现代C++编程的发展,提升了开发效率与程序性能。
579 12
|
编译器 程序员 定位技术
C++ 20新特性之Concepts
在C++ 20之前,我们在编写泛型代码时,模板参数的约束往往通过复杂的SFINAE(Substitution Failure Is Not An Error)策略或繁琐的Traits类来实现。这不仅难以阅读,也非常容易出错,导致很多程序员在提及泛型编程时,总是心有余悸、脊背发凉。 在没有引入Concepts之前,我们只能依靠经验和技巧来解读编译器给出的错误信息,很容易陷入“类型迷路”。这就好比在没有GPS导航的年代,我们依靠复杂的地图和模糊的方向指示去一个陌生的地点,很容易迷路。而Concepts的引入,就像是给C++的模板系统安装了一个GPS导航仪
525 59
|
存储 算法 安全
基于哈希表的文件共享平台 C++ 算法实现与分析
在数字化时代,文件共享平台不可或缺。本文探讨哈希表在文件共享中的应用,包括原理、优势及C++实现。哈希表通过键值对快速访问文件元数据(如文件名、大小、位置等),查找时间复杂度为O(1),显著提升查找速度和用户体验。代码示例展示了文件上传和搜索功能,实际应用中需解决哈希冲突、动态扩容和线程安全等问题,以优化性能。
|
算法 编译器 C++
【C++11】lambda表达式
C++11 引入了 Lambda 表达式,这是一种定义匿名函数的方式,极大提升了代码的简洁性和可维护性。本文详细介绍了 Lambda 表达式的语法、捕获机制及应用场景,包括在标准算法、排序和事件回调中的使用,以及高级特性如捕获 `this` 指针和可变 Lambda 表达式。通过这些内容,读者可以全面掌握 Lambda 表达式,提升 C++ 编程技能。
880 3
|
存储 算法 搜索推荐
对二叉堆的简单分析,c和c++的简单实现
这篇文章提供了对二叉堆数据结构的简单分析,并展示了如何在C和C++中实现最小堆,包括初始化、插入元素、删除最小元素和打印堆的函数,以及一个示例程序来演示这些操作。
281 19
|
安全 编译器 C++
【C++11】新特性
`C++11`是2011年发布的`C++`重要版本,引入了约140个新特性和600个缺陷修复。其中,列表初始化(List Initialization)提供了一种更统一、更灵活和更安全的初始化方式,支持内置类型和满足特定条件的自定义类型。此外,`C++11`还引入了`auto`关键字用于自动类型推导,简化了复杂类型的声明,提高了代码的可读性和可维护性。`decltype`则用于根据表达式推导类型,增强了编译时类型检查的能力,特别适用于模板和泛型编程。
300 2
|
Ubuntu Linux Shell
C++ 之 perf+火焰图分析与调试
【11月更文挑战第6天】在遇到一些内存异常的时候,经常这部分的代码是很难去进行分析的,最近了解到Perf这个神器,这里也展开介绍一下如何使用Perf以及如何去画火焰图。
805 5
|
Ubuntu Linux Shell
C++ 之 perf+火焰图分析与调试
【10月更文挑战第8天】在遇到一些内存异常的时候,经常这部分的代码是很难去进行分析的,最近了解到Perf这个神器,这里也展开介绍一下如何使用Perf以及如何去画火焰图。
449 1
|
存储 编译器 C++
【C++】面向对象编程的三大特性:深入解析多态机制(三)
【C++】面向对象编程的三大特性:深入解析多态机制
232 1