What is the lifetime of a C++ lambda expression?
(我已经阅读了C ++中lambda派生的隐式仿函数的生命周期是什么?已经没有回答这个问题了。)
我理解C ++ lambda语法只是用于创建具有调用操作符和某个状态的匿名类的实例的糖,并且我理解该状态的生存期要求(由您是否通过引用的值捕获来决定。)但是什么是 lambda对象本身的生命周期? 在以下示例中,返回的std::function实例是否有用?
1 2 3 4
| std::function<int(int)> meta_add(int x) {
auto add = [x](int y) { return x + y; };
return add;
} |
如果是,它是如何工作的? 这对我来说似乎有点太神奇了 - 我只能想象它是通过std::function复制我的整个实例来工作的,这可能是非常沉重的,这取决于我捕获的内容 - 过去我主要使用std::function使用裸功能 指针,复制它们很快。 鉴于std::function的类型擦除,它似乎也有问题。
如果用手动编织器代替lambda,它的生命周期正是如此:
1 2 3 4 5 6 7 8 9 10 11 12
| 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函数的本地创建,然后[在其entirty中,包括x的值]移动到返回值中,然后本地实例将超出范围并正常销毁。但是只要保存它的std::function对象,该函数返回的对象将保持有效。这多长时间显然取决于调用上下文。
-
问题是,我不知道这实际上也适用于命名类,并且它让我感到困惑。
-
如果您不熟悉C ++ 11之前函数对象的工作方式,那么您应该看一下这些函数对象,因为lambdas几乎不是函数对象的语法糖。一旦你理解了lambdas与函数对象具有相同的值语义,那么它们的生命周期是相同的。
-
正常(在堆栈上分配)生命周期,但返回值优化?
-
不会在返回时添加复制,而不是移动?一个真正的lambda会被移动吗?我不知道为什么它不能成为任何理由,但也许那不是它实际上是如何工作的?
看起来你对std::function比对lambdas更困惑。
std::function使用称为类型擦除的技术。这是一个快速飞过。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| 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)。
1 2 3 4 5 6 7 8 9 10 11
| 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基本上是传递它们的唯一方法(如果我有一个命名类型,Id只返回一个命名类型的实例,而且相当明显),然后我不知道实例去了哪里。
-
这只是示例代码,缺少一些细节。 它泄漏了内存,并且缺少对std::move,std::forward的调用。 此外,std::function通常使用小对象优化来避免在T较小时使用堆。
-
对不起,我试着去学习什么类型的擦除,并且在你的例子中,Erased你有m_erased.call(f)。 如果m_erased是成员指针,你怎么能做m_erased.call(f)? 我试图将点更改为箭头,我认为它试图访问Base纯虚函数。 这是因为它只是一个例子吗? 我一直盯着它看了十分钟,我觉得我很生气。 谢谢
-
@TitoneMaurice:是的,那绝对应该是->。 为什么你认为它会调用基本虚函数? 请记住,即使派生类省略了virtual关键字,虚函数也会被重载
这是:
1
| [x](int y) { return x + y; }; |
相当于:(或者也可以考虑)
1 2 3 4 5 6 7
| 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不是一回事;它只是一种在对象中包装泛型callable的方法。
-
@NicolBolas:嗯,我通过std :: function返回了一个原因,因为这是我不理解的步骤。正如丹尼斯所说,这也适用于我不知道的命名类 - 对于过去的一年(在我开始使用std :: function之后但在我开始使用lambdas之前),我总是认为它不会起作用。
-
lambda将被移动到std::function<>实例中,而不是被复制,因此具有明确定义的复制构造函数是无关紧要的 - 移动构造函数是什么相关的。
-
嗯,如果是这样,那么我不能做(新MyLamba(x))(y)?我不认为这是可能的。
-
另外,移动而不是复制呢?看起来如果lambda副本捕获许多值,移动和复制之间的差异将是显着的。
-
@allyourcode:评论一。你可以做(new MyLambda(x))->operator()(y)。你通常做的是MyLambda(x)(y)。 new的问题在于您创建的指针不是对象。指针没有方法,因此您需要将指针取消引用到对象类型。
-
@allyourcode:在大多数情况下,复制elidtion(sp)会使副本无效。但要真正回答你需要更加具体地考虑你正在考虑的情况,因为它可能是一个问题。
-
@LokiAstari啊是的。不知道我为什么用新的。但这不是我的观点:构造函数似乎不存在。为什么我尝试使用它,我的编译器告诉我只有两个构造函数,并且它们都没有使用x。
在您发布的代码中:
1 2 3 4
| std::function<int(int)> meta_add(int x) {
auto add = [x](int y) { return x + y; };
return add;
} |
函数返回的std::function对象实际上包含已分配给局部变量add的lambda函数对象的移动实例。
当您定义捕获按值或按引用的C ++ 11 lambda时,C ++编译器会自动生成一个唯一的函数类型,其实例是在调用lambda或赋值给变量时构造的。为了说明,您的C ++编译器可能会为[x](int y) { return x + y; }定义的lambda生成以下类类型:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| class __lambda_373s27a
{
int x;
public:
__lambda_373s27a(int x_)
: x(x_)
{
}
int operator()(int y) const {
return x + y;
}
}; |
然后,meta_add函数基本上等同于:
1 2 3 4
| std::function<int(int)> meta_add(int x) {
__lambda_373s27a add = __lambda_373s27a(x);
return add;
} |
编辑:顺便说一句,我不知道你是否知道这一点,但这是C ++ 11中函数currying的一个例子。
-
实际上,函数返回的std::function对象包含一个移动的lambda函数对象实例 - 不执行任何副本。
-
ildjarn:meta_add(int)函数是否需要返回std::move(add)才能调用函数类型的移动构造函数(在本例中为__lambda_373s27a)?
-
不,return语句允许隐式地将返回值视为右值,使其可以隐式移动并且不需要显式return std::move(...);(这可以防止RVO / NRVO,实际上使return std::move(...);成为反模式)。因为add在return语句中被视为rvalue,因此lambda被移动到std::function<>构造函数参数中。