C++ lambda 表达式的生命周期是多少?

Posted

技术标签:

【中文标题】C++ lambda 表达式的生命周期是多少?【英文标题】:What is the lifetime of a C++ lambda expression? 【发布时间】:2011-12-17 23:59:34 【问题描述】:

(我已经阅读了What is the lifetime of lambda-derived implicit functors in C++?,它没有回答这个问题。)

我了解 C++ lambda 语法只是用于创建具有调用运算符和某些状态的匿名类的实例的糖,并且我了解该状态的生命周期要求(取决于您是否通过引用的值捕获。)但是 lambda 对象本身的生命周期是多少?在以下示例中,返回的 std::function 实例是否有用?

std::function<int(int)> meta_add(int x) 
    auto add = [x](int y)  return x + y; ;
    return add;

如果是,它是如何工作的?这对我来说似乎有点太神奇了——我只能想象它通过 std::function 复制我的整个实例来工作,根据我捕获的内容,这可能非常重——过去我主要使用了 std::function 与裸函数指针,复制它们很快。鉴于std::function 的类型擦除,这似乎也有问题。

【问题讨论】:

【参考方案1】:

生命周期与将 lambda 替换为手动仿函数的情况完全相同:

struct lambda 
   lambda(int x) : x(x)  
   int operator ()(int y)  return x + y; 

private:
   int x;
;

std::function<int(int)> meta_add(int x) 
   lambda add(x);
   return add;

对象将被创建,在 meta_add 函数的本地,然后将 [in its entirty, including the value of x] 移动到返回值中,然后本地实例将超出范围并被销毁为普通的。但只要持有它的std::function 对象,从函数返回的对象将保持有效。这显然取决于调用上下文。

【讨论】:

问题是,我不知道这实际上也适用于命名类,它让我无所适从。 如果你不熟悉 C++11 之前的函数对象是如何工作的,你应该看看那些,因为 lambda 几乎只是函数对象的语法糖。一旦你明白了,很明显 lambda 具有与函数对象相同的值语义,因此它们的生命周期是相同的。 正常(在堆栈上分配)生命周期,但返回值优化? 不会在返回时添加被复制,而不是移动?一个真正的 lambda 会被移动吗?我不知道为什么不能这样,但也许它实际上不是这样工作的??【参考方案2】:

您似乎对 std::function 比对 lambdas 更困惑。

std::function 使用一种称为类型擦除的技术。快速浏览一下。

class Base

  virtual ~Base() 
  virtual int call( float ) =0;
;

template< typename T>
class Eraser : public Base

public:
   Eraser( T t ) : m_t(t)  
   virtual int call( float f ) override  return m_t(f); 
private:
   T m_t;
;

class Erased

public:
   template<typename T>
   Erased( T t ) : m_erased( new Eraser<T>(t) )  

   int do_call( float f )
   
      return m_erased->call( f );
   
private:
   Base* m_erased;
;

为什么要擦除类型?我们想要的类型不就是int (*)(float)吗?

类型擦除允许Erased 现在可以存储任何可调用的值,例如int(float)

int boring( float f);
short interesting( double d );
struct Powerful

   int operator() ( float );
;

Erased e_boring( &boring );
Erased e_interesting( &interesting );
Erased e_powerful( Powerful() );
Erased e_useful( []( float f )  return 42;  );

【讨论】:

回想起来,我对 std::function 感到困惑,因为我不知道它保留了很多东西的所有权。我假设在实例离开范围后将实例“包装”到 std::function 中是无效的。 lambdas 引起混乱的原因是 std::function 基本上是传递它们的唯一方法(如果我有一个命名类型,我只会返回一个命名类型的实例,这很明显) ,然后我不知道实例去了哪里。 这只是示例代码,缺少一些细节。它会泄漏内存,并且缺少对std::movestd::forward 的调用。还有std::function一般会使用小对象优化来避免T小的时候使用堆。 对不起,我正在尝试了解擦除类型是什么,并且在您的示例中擦除类中您有 m_erased.call(f)。如果 m_erased 是一个成员指针,你怎么能做 m_erased.call(f)?我试图将点更改为箭头,我认为它正在尝试访问 Base 纯虚函数。这是因为这只是一个例子吗?我已经盯着它看了十分钟,我想我快疯了。谢谢 @TitoneMaurice:是的,那绝对应该是@​​987654332@。为什么你认为它会调用基础虚函数?请记住,即使派生类省略了virtual 关键字,虚函数也会被重载【参考方案3】:

这是:

[x](int y)  return x + y; ;

相当于:(或者也可以考虑)

struct MyLambda

    MyLambda(int x): x(x) 
    int operator()(int y) const  return x + y; 
private:
    int x;
;

所以你的对象正在返回一个看起来像这样的对象。它有一个定义明确的复制构造函数。所以它可以正确地从函数中复制出来,这似乎是非常合理的。

【讨论】:

要将其从函数中复制出来需要 std::function 知道它的类型 std::function 已被实例化。它怎么能做到这一点?唯一想到的技巧是一个指向模板类实例的指针,该模板具有一个知道 lambda 确切类型的虚函数。这看起来很讨厌,我什至不知道它是否真的有效。 @Joe:您基本上描述了您的磨机类型擦除操作,这正是它的工作原理。 @JoeWreschnig:你没有问std::function 是如何工作的;你问过 lambdas 是如何工作的。 std::function 不是一回事;这只是一种将通用可调用对象包装在对象中的方法。 @NicolBolas:嗯,我通过 std::function 返回是有原因的,因为那是我不明白的步骤。正如丹尼斯所说,这也适用于命名类,我不知道 - 大约在过去的一年里(在我开始使用 std::function 之后但在我开始使用 lambdas 之前)我一直认为它不起作用。跨度> lambda 将被移动到 std::function&lt;&gt; 实例中,而不是被复制,因此拥有一个定义良好的复制构造函数是无关紧要的 - move 构造函数是相关的。 【参考方案4】:

在您发布的代码中:

std::function<int(int)> meta_add(int x) 
    auto add = [x](int y)  return x + y; ;
    return add;

函数返回的 std::function&lt;int(int)&gt; 对象实际上保存了一个已移动的 lambda 函数对象实例,该实例已分配给局部变量 add

当您定义捕获按值或按引用的 C++11 lambda 时,C++ 编译器会自动生成唯一的函数类型,该类型的实例是在调用 lambda 或将其分配给变量时构造的。为了说明,您的 C++ 编译器可能会为 [x](int y) return x + y; 定义的 lambda 生成以下类类型:

class __lambda_373s27a

    int x;

public:
    __lambda_373s27a(int x_)
        : x(x_)
    
    

    int operator()(int y) const 
        return x + y;
    
;

那么,meta_add 函数本质上等价于:

std::function<int(int)> meta_add(int x) 
    __lambda_373s27a add = __lambda_373s27a(x);
    return add;

编辑:顺便说一句,我不确定你是否知道,但这是 C++11 中函数 currying 的示例。

【讨论】:

实际上,该函数返回的std::function&lt;int(int)&gt; 对象包含一个已移动 lambda 函数对象的实例——不执行任何副本。 ildjarn:meta_add(int) 函数是否需要返回std::move(add) 才能调用函数类型的移动构造函数(在这种情况下为__lambda_373s27a)? 不,return 语句允许隐式地将返回的值视为右值,使其隐式可移动并消除对显式 return std::move(...); 的需要(防止 RVO/NRVO,实际上使 @ 987654335@ 反模式)。因此,因为 addreturn 语句中被视为右值,因此 lambda 被移动到 std::function&lt;&gt; 构造函数参数中。

以上是关于C++ lambda 表达式的生命周期是多少?的主要内容,如果未能解决你的问题,请参考以下文章

C++ 函数中静态变量的生命周期是多少?

临时人员的 C++ 生命周期 - 这安全吗?

Vue:基础语法、创建组件、组件间传值、实例生命周期

c++类中 各种成员的生命周期?

lambda中对象的生命周期连接到pyqtSignal

会话的默认生命周期是多少?