c++,lambda是匿名函数也可能不是函数,从汇编底层角度深入理解带捕获的lambda如何转化为std::function

发布于:2025-07-24 ⋅ 阅读:(11) ⋅ 点赞:(0)

开始

本章内容需要以下几篇内容作为基础:
《c++,从汇编角度看lambda》
《c++,stl库阅读之std::forword完美转发》

std::function 是什么魔法?

它是一个类吗,类里有什么?
是一个编译器对函数指针的“别名”吗,只是概念的东西吗?

示例代码:

#include <iostream>
#include <functional>

typedef int (*func)();

void callFunc(std::function<int()>&& f) {
    f();
}

int main(int args, char* argv[]) {
    int a = 1, b = 2, c = 3;

    callFunc(std::function<int()>([a, b, c]() -> int {
        std::cout << b << c << std::endl;
        return a;
    }));
}

汇编中无关内容有省略

模板中的完美转发部分省略。完美转发被编译为函数,但对应gcc实现完美转发而言,无论是左值引用还是右值引用都是指针没有区别,所以完美转发函数的内容一般是,传参什么,返回什么。。。

main

先看main函数的汇编

000000000040120a <main>:
int main(int args, char* argv[]) {
 # 申请0x58大小的栈空间,存储 a b c 三个变量和 function 类
  40120f:	48 83 ec 58          	sub    $0x58,%rsp

  # -0x28(%rbp) 开始的空间里,把 abc 三个变量即立即数 1 2 3 放进去
    int a = 1, b = 2, c = 3;
  40121a:	c7 45 ec 01 00 00 00 	movl   $0x1,-0x14(%rbp)
  401221:	c7 45 e8 02 00 00 00 	movl   $0x2,-0x18(%rbp)
  401228:	c7 45 e4 03 00 00 00 	movl   $0x3,-0x1c(%rbp)
    callFunc(std::function<int()>([a, b, c]() -> int {
  40122f:	8b 45 ec             	mov    -0x14(%rbp),%eax
  401232:	89 45 d8             	mov    %eax,-0x28(%rbp)
  401235:	8b 45 e8             	mov    -0x18(%rbp),%eax
  401238:	89 45 dc             	mov    %eax,-0x24(%rbp)
  40123b:	8b 45 e4             	mov    -0x1c(%rbp),%eax
  40123e:	89 45 e0             	mov    %eax,-0x20(%rbp)
  
  # 调用function的构造,传递的this是当前栈空间地址 -0x50(%rbp)
  # 第二个参数是 -0x28(%rbp),即,捕获的 a b c,排排站的可整体空间的地址!
  401241:	48 8d 55 d8          	lea    -0x28(%rbp),%rdx
  401245:	48 8d 45 b0          	lea    -0x50(%rbp),%rax
  401249:	48 89 d6             	mov    %rdx,%rsi
  40124c:	48 89 c7             	mov    %rax,%rdi
  40124f:	e8 40 00 00 00       	call   401294 <std::function<int ()>::function<main::{lambda()#1}, void>(main::{lambda()#1}&&)>

main函数得知,function是一个类

奇葩的是,function的构造,传递的却是一个所有捕获变量的空间地址,却不是lambda函数地址。。。

function 右值引用函数构造

先看以下main函数调用的function 右值引用函数构造源码:

    using _Handler
	  = _Function_handler<_Res(_ArgTypes...), __decay_t<_Functor>>;
	  
      // _GLIBCXX_RESOLVE_LIB_DEFECTS
      // 2774. std::function construction vs assignment
      template<typename _Functor,
	       typename _Constraints = _Requires<_Callable<_Functor>>>
	function(_Functor&& __f)
	noexcept(_Handler<_Functor>::template _S_nothrow_init<_Functor>())
	: _Function_base()
	{
	  using _My_handler = _Handler<_Functor>;

	  if (_My_handler::_M_not_empty_function(__f))
	    {
	      // 上面密密麻麻,关键就在这里三行,等一下汇编看这一行
	      _My_handler::_M_init_functor(_M_functor,
					   std::forward<_Functor>(__f));

		  // 这两行只是保存以下地址
	      _M_invoker = &_My_handler::_M_invoke;
	      _M_manager = &_My_handler::_M_manager;
	    }
	}

看一下汇编:

0000000000401294 <std::function<int ()>::function<main::{lambda()#1}, void>(main::{lambda()#1}&&)>:
       */
      // _GLIBCXX_RESOLVE_LIB_DEFECTS
      // 2774. std::function construction vs assignment
      template<typename _Functor,
	       typename _Constraints = _Requires<_Callable<_Functor>>>
	function(_Functor&& __f)

  # 这一段内容比较简单,调用std::_Function_base::_Base_manager<main::{lambda()#1}>::_M_init_functor 时候传递两个参数,一个是this,一个是function构造时候传递的捕获参数的空间地址
  # 就像是 f(addr) { _M_init_functor(addr);} 一样
  40129c:	48 89 7d f8          	mov    %rdi,-0x8(%rbp)
  4012a0:	48 89 75 f0          	mov    %rsi,-0x10(%rbp)
  4012e8:	48 8b 45 f0          	mov    -0x10(%rbp),%rax
  4012f4:	48 89 c2             	mov    %rax,%rdx
  4012f7:	48 8b 45 f8          	mov    -0x8(%rbp),%rax
  4012fb:	48 89 d6             	mov    %rdx,%rsi
  4012fe:	48 89 c7             	mov    %rax,%rdi
  401301:	e8 38 00 00 00       	call   40133e <void std::_Function_base::_Base_manager<main::{lambda()#1}>::_M_init_functor<main::{lambda()#1}>(std::_Any_data&, main::{lambda()#1}&&)>
					   std::forward<_Functor>(__f));

  # $0x40136f 函数是 std::_Function_handler<int (), main::{lambda()#1}>::_M_invoke(std::_Any_data const&)
  # 注意,这个invoke 是一个专属的 lambda()# 的 invoke
	      _M_invoker = &_My_handler::_M_invoke;
  401306:	48 8b 45 f8          	mov    -0x8(%rbp),%rax
  40130a:	48 c7 40 18 6f 13 40 	movq   $0x40136f,0x18(%rax)
  401311:	00 

_Function_base::_M_init_functor

看以下源码:

class _Function_base
  {
  
	template<typename _Fn>
	  static void
	  _M_init_functor(_Any_data& __functor, _Fn&& __f)
	  noexcept(__and_<_Local_storage,
			  is_nothrow_constructible<_Functor, _Fn>>::value)
	  {
	    _M_create(__functor, std::forward<_Fn>(__f), _Local_storage());
	  }

没什么内容,正常调用_M_create

汇编一样,没什么内容

000000000040133e <void std::_Function_base::_Base_manager<main::{lambda()#1}>::_M_init_functor<main::{lambda()#1}>(std::_Any_data&, main::{lambda()#1}&&)>:
	  _M_init_functor(_Any_data& __functor, _Fn&& __f)
  401342:	48 83 ec 10          	sub    $0x10,%rsp
  401346:	48 89 7d f8          	mov    %rdi,-0x8(%rbp)
  40134a:	48 89 75 f0          	mov    %rsi,-0x10(%rbp)
	    _M_create(__functor, std::forward<_Fn>(__f), _Local_storage());
  40134e:	48 8b 45 f0          	mov    -0x10(%rbp),%rax
  40135a:	48 89 c2             	mov    %rax,%rdx
  40135d:	48 8b 45 f8          	mov    -0x8(%rbp),%rax
  401361:	48 89 d6             	mov    %rdx,%rsi
  401364:	48 89 c7             	mov    %rax,%rdi
  401367:	e8 c1 00 00 00       	call   40142d <void std::_Function_base::_Base_manager<main::{lambda()#1}>::_M_create<main::{lambda()#1}>(std::_Any_data&, main::{lambda()#1}&&, std::integral_constant<bool, true>)>
	  }

_Function_base::_Base_manager::_M_create

看一下源码:

	// Construct a location-invariant function object that fits within
	// an _Any_data structure.
	template<typename _Fn>
	  static void
	  _M_create(_Any_data& __dest, _Fn&& __f, true_type)
	  {
	    ::new (__dest._M_access()) _Functor(std::forward<_Fn>(__f));
	  }

来啦!
调用了lambda函数(_Functor(std::forward<_Fn>(__f))的构造函数!

lambda函数,转化为function,编译器帮我们把lambda变成了一个类!这个类有构造函数!

题外话:

这种情况下,fakeFunc还是一个函数,构造function时候传递的是函数地址,_M_create时候不会执行构造函数

int fakeFunc() {return 0;};

std::function<int()>(fakeFunc);

回到正文,继续看汇编

000000000040142d <void std::_Function_base::_Base_manager<main::{lambda()#1}>::_M_create<main::{lambda()#1}>(std::_Any_data&, main::{lambda()#1}&&, std::integral_constant<bool, true>)>:
	  _M_create(_Any_data& __dest, _Fn&& __f, true_type)
  40143a:	48 89 75 e0          	mov    %rsi,-0x20(%rbp)
  40145a:	48 8b 45 e0          	mov    -0x20(%rbp),%rax
  401466:	48 89 c6             	mov    %rax,%rsi
  40146c:	e8 99 ff ff ff       	call   40140a <main::{lambda()#1}::main({lambda()#1}&&)>

其他都不重要了,仅仅是正常调用lambda的构造,传递的是捕获参数的空间地址

lambda::lambda

直接看汇编,因为,没有源码。。。

000000000040140a <main::{lambda()#1}::main({lambda()#1}&&)>:
    callFunc(std::function<int()>([a, b, c]() -> int {
  40140a:	55                   	push   %rbp
  40140b:	48 89 e5             	mov    %rsp,%rbp
  40140e:	48 89 7d f8          	mov    %rdi,-0x8(%rbp)          # lambda 类的 this
  401412:	48 89 75 f0          	mov    %rsi,-0x10(%rbp)         # rsi a,b,c 值空间地址
  401416:	48 8b 45 f8          	mov    -0x8(%rbp),%rax
  40141a:	48 8b 55 f0          	mov    -0x10(%rbp),%rdx
  40141e:	48 8b 0a             	mov    (%rdx),%rcx              
  401421:	48 89 08             	mov    %rcx,(%rax)              # 从 a,b,c 值空间地址,拷贝内容到 this
  401424:	8b 52 08             	mov    0x8(%rdx),%edx
  401427:	89 50 08             	mov    %edx,0x8(%rax)           # 第二次拷贝,三个int类型占12字节

哇,多么朴实无华的一个类构造函数,lambda类不仅有构造函数,还有自己的空间

就像下面的伪代码一样

class lambda {
    struct {int a, int b, int c} Args;      // 声明捕获内容

    Args args;                              // 捕获参数存储在类里

    lambda(Args& args) {                    // 构造,复制捕获内容
        this.args = args;
    }
}

callFunc

接下来看function如何执行的

先看callFunc,内容朴实无华,调用function的操作符()函数

void callFunc(std::function<int()>&& f) {
    f();
}
void callFunc(std::function<int()>&& f) {
  4011b9:	e8 02 06 00 00       	call   4017c0 <std::function<int ()>::operator()() const>
}

std::function<int ()>::operator()

先看下源码:

      _Res
      operator()(_ArgTypes... __args) const
      {
	if (_M_empty())
	  __throw_bad_function_call();
	return _M_invoker(_M_functor, std::forward<_ArgTypes>(__args)...);
      }

执行_M_invoker函数,这个函数正是function 右值引用函数构造章节中保存的一个地址

// 这两行只是保存以下地址
	      _M_invoker = &_My_handler::_M_invoke;

当时保存在哪了呢?

  # $0x40136f 函数是 std::_Function_handler<int (), main::{lambda()#1}>::_M_invoke(std::_Any_data const&)
  # 注意,这个invoke 是一个专属的 lambda()# 的 invoke
	      _M_invoker = &_My_handler::_M_invoke;
  401306:	48 8b 45 f8          	mov    -0x8(%rbp),%rax
  40130a:	48 c7 40 18 6f 13 40 	movq   $0x40136f,0x18(%rax)

正是0x18(%rax)

和下面汇编的rdx位置对应

00000000004017c0 <std::function<int ()>::operator()() const>:
  4017c8:	48 89 7d f8          	mov    %rdi,-0x8(%rbp)
      {
	if (_M_empty())
  4017cc:	48 8b 45 f8          	mov    -0x8(%rbp),%rax
  4017e5:	48 8b 50 18          	mov    0x18(%rax),%rdx
  4017e9:	48 8b 45 f8          	mov    -0x8(%rbp),%rax
  4017ed:	48 89 c7             	mov    %rax,%rdi
  4017f0:	ff d2                	call   *%rdx                # rdx = this + 0x18

接下来,看当时保存的lambda::_M_invoke函数

# readelf -s -W main |c++filt |grep 40136f
    19: 000000000040136f    34 FUNC    LOCAL  DEFAULT   14 std::_Function_handler<int (), main::{lambda()#1}>::_M_invoke(std::_Any_data const&)

而剩下的,都是编译器生成的内容了:

lambda::_M_invoke -> lambda::operate()

_M_invoke -> __invoke_r -> __invoke_impl -> lambda::operate()

中间的一段invoke是编译器生成的,不确定有没有源码,不重要,中间都是一些完美转发、参数传递的内容

最终,调用到了lambda类的::operate()位置:

看一下lambda的真正内容

    callFunc(std::function<int()>([a, b, c]() -> int {
  4011c6:	48 83 ec 10          	sub    $0x10,%rsp
  4011ca:	48 89 7d f8          	mov    %rdi,-0x8(%rbp)
  4011ce:	48 8b 45 f8          	mov    -0x8(%rbp),%rax          # rax = this
  4011d2:	8b 40 04             	mov    0x4(%rax),%eax           # this + 4
        std::cout << b << c << std::endl;
  4011d5:	89 c6                	mov    %eax,%esi                # a = this + 4
  4011d7:	bf 80 40 40 00       	mov    $0x404080,%edi
  4011dc:	e8 af fe ff ff       	call   401090 <std::basic_ostream<char, std::char_traits<char> >::operator<<(int)@plt>

a = this + 4,还记得吗,a,b,c变量在lambda构造时被复制过来了,这段operate()非常朴实无华,使用捕获的参数,就像使用类的参数一模一样。

end

std::function构造带捕获的lambda时,lamda变成了一个类,构造传参是捕获参数,std::function构造时会执行lambda构造函数b并会将捕获参数保存,lambda函数内容变成operator()函数;std::function被传入的是lambda类的传参。

又一个非常刁钻,非常有意思的面试题出现了,lambda匿名函数是函数吗?

考察了对lambda捕获、std::function的原理。


网站公告

今日签到

点亮在社区的每一天
去签到