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


70

(我已经阅读了lambda派生的隐式函子在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类型的擦除,这似乎也有问题。

Answers:


64

如果您将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函数本地创建对象,然后将其完整移动(包括[ x]的值)到返回值中,然后本地实例将超出范围并被正常销毁。但是,从函数返回的对象将一直保持有效,直到std::function保存它的对象为止。显然,要多长时间取决于调用上下文。


1
问题是,我也不知道它实际上也不能用于命名类,并且使我无所适从。

4
如果您不熟悉C ++ 11之前的函数对象的工作方式,那么您应该看一下这些对象,因为lambda几乎只是函数对象的语法。一旦您了解了lambda与函数对象具有相同的值语义,这很明显,因此它们的生存期也相同。
bames53

正常(在栈上分配)生存期,但具有返回值优化功能?
2012年

不会在返回时复制,不移动吗?真正的lambda是否会被移动?我不知道为什么不能这样做,但是也许那实际上并不是它的工作原理??
allyourcode

18

似乎您std::function比lambda更困惑。

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; } );

3
回想起来,我对std :: function感到困惑,因为我不知道它保留了任何东西的所有权。我以为在实例离开作用域之后,将实例“包装”到std :: function无效。lambdas引起混乱的原因是因为std :: function基本上是传递它们的唯一方法(如果我有一个命名类型,我将只返回一个命名类型的实例,并且很明显地起作用) ,然后我不知道实例在哪里。

2
这只是示例代码,缺少一些细节。它泄漏内存,它的失踪通话std::movestd::forward。还std::function通常使用的小物体的优化,以避免当使用堆T小。
deft_code 2011年

1
对不起,我正在尝试学习什么是类型擦除,在您的Erased类示例中,您有m_erased.call(f)。如果m_erased是成员指针,该怎么做m_erased.call(f)?我试图将点更改为箭头,并且我认为它正在尝试访问Base纯虚拟函数。是因为这只是一个例子?我盯着它看了十分钟,我想我快要疯了。谢谢
Zebrafish

1
@TitoneMaurice:是的,绝对应该是->。您为什么认为它将调用基本虚函数?请记住,即使派生类省略了virtual关键字,虚拟函数也会被重载
Eric

12

这是:

[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的确切类型。这看起来很讨厌,我什至不知道它是否真的有效。

2
@Joe:您基本上描述了磨粉机类型擦除的运行,这就是它的工作原理。
2011年

1
@JoeWreschnig:您没有询问std::function工作原理;您问过lambda如何运作。std::function不一样 这只是在对象中包装通用可调用对象的一种方法。
Nicol Bolas

1
@NicolBolas:好吧,我出于某种原因通过std :: function返回,因为那是我不理解的步骤。正如Dennis所说,这也适用于命名类,而我在过去的一年中(在我开始使用std :: function之后但在开始使用lambda之前)一直不知道这是什么,我一直以为这是行不通的。

Lambda将被移动到std::function<>实例中,而不是被复制,因此没有一个明确定义的复制构造函数是无关紧要的-移动构造函数是相关的。
ildjarn 2011年

4

在您发布的代码中:

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

std::function<int(int)>函数返回的对象实际上包含已分配给局部变量的lambda函数对象的移动实例add

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

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中函数循环的示例。


实际上,该std::function<int(int)>函数返回的对象保留了lambda函数对象的移动实例-不执行任何复制。
ildjarn 2011年

ildjarn:调用函数类型的move构造函数(在这种情况下)是否meta_add(int)不需要返回std::move(add)该函数__lambda_373s27a
Daniel Trebbien

3
不,return允许语句将返回的值隐式地视为一个右值,使其隐式可移动,并且不需要显式的return std::move(...);(这避免了RVO / NRVO,实际上是制作return std::move(...);了反模式)。因此,由于addreturn语句中将其视为右值,因此lambda因此被移入了std::function<>构造函数参数。
ildjarn
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.