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::move
、std::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<>
实例中,而不是被复制,因此拥有一个定义良好的复制构造函数是无关紧要的 - move 构造函数是相关的。
【参考方案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++ 编译器可能会为 [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<int(int)>
对象包含一个已移动 lambda 函数对象的实例——不执行任何副本。
ildjarn:meta_add(int)
函数是否需要返回std::move(add)
才能调用函数类型的移动构造函数(在这种情况下为__lambda_373s27a
)?
不,return
语句允许隐式地将返回的值视为右值,使其隐式可移动并消除对显式 return std::move(...);
的需要(防止 RVO/NRVO,实际上使 @ 987654335@ 反模式)。因此,因为 add
在 return
语句中被视为右值,因此 lambda 被移动到 std::function<>
构造函数参数中。以上是关于C++ lambda 表达式的生命周期是多少?的主要内容,如果未能解决你的问题,请参考以下文章