如果 lambda 在运行时被移动/破坏会发生啥?
Posted
技术标签:
【中文标题】如果 lambda 在运行时被移动/破坏会发生啥?【英文标题】:What happens if a lambda is moved/destructed while it is running?如果 lambda 在运行时被移动/破坏会发生什么? 【发布时间】:2015-07-18 17:38:12 【问题描述】:考虑:
std::vector<std::function<void()>> vec;
something_unmovable m;
vec.push_back([&vec, m]()
vec.resize(100);
// things with 'm'
);
vec[0]();
vec.resize(100)
可能会导致向量的重新分配,这意味着std::function
s 将被复制到一个新位置,而旧的将被销毁。然而,当旧的仍在运行时,就会发生这种情况。这个特定的代码运行是因为 lambda 没有做任何事情,但我想这很容易导致未定义的行为。
那么,究竟发生了什么? m
仍然可以从向量中访问吗?或者是 lambda 的 this
指针现在无效(指向已释放的内存),因此 lambda 捕获的任何内容都无法访问,但是如果它运行的代码不使用它捕获的任何内容,它不是未定义的行为?
另外,lambda 可移动的情况有什么不同吗?
【问题讨论】:
“不可移动”是否意味着“不可复制”,还是m
可复制? (通常来说,如果某些东西是可复制的,那么它会自动移动,因为副本是移动的有效实现。)您不能制作捕获不可复制值的 lambda (demo)。
我认为“不可移动”是指仅删除了其他默认的移动构造函数。在这些假设下可以制作一个。我认为问题的重点是解决当捕获本身被破坏时 lambda 捕获的内容会发生什么。 AFAIK,它们的处理方式与结构相同。
【参考方案1】:
正如其他答案已经涵盖的那样,lambda 本质上是语法糖,用于轻松创建提供自定义 operator()
实现的类型。这就是为什么您甚至可以使用对 operator()
的显式引用来编写 lambda 调用,例如:int main() return []() return 0; .operator()();
。所有非静态成员函数的相同规则也适用于 lambda 主体。
并且这些规则允许在执行成员函数时销毁对象,只要成员函数之后不使用this
。您的示例是一个不寻常的示例,更常见的示例是执行delete this;
的非静态成员函数。 This made it into the C++ FAQ,说明是允许的。
据我所知,该标准通过不真正解决它来实现这一点。它以不依赖于不被销毁的对象的方式描述成员函数的语义,因此实现必须确保即使对象被销毁也让成员函数继续执行。
所以回答你的问题:
或者是 lambda 的 this 指针现在无效(指向已释放的内存),因此 lambda 捕获的任何内容都无法访问,但如果它运行的代码不使用它捕获的任何内容,这不是未定义的行为?
是的,差不多。
另外,lambda 可移动的情况有什么不同吗?
不,不是。
唯一可能影响 lambda 移动的时间是在移动 lambda 之后。在您的示例中,operator()
继续在原始移出然后销毁的仿函数上执行。
【讨论】:
【参考方案2】:您可以将 lambda 捕获视为普通结构实例。
在你的情况下:
struct lambda_UUID_HERE_stuff
std::vector<std::function<void()>> &vec;
something_unmovable m;
void operator()()
this->vec.resize(100);
;
...我相信所有相同的规则都适用(就 VS2013 而言)。
因此,这似乎是另一种未定义行为的情况。也就是说,如果&vec
恰好指向包含捕获实例的向量,并且operator()
中的操作会导致该向量调整大小。
【讨论】:
这是一个相当空洞的内容,除非你证明它是结构的 UB。 @LightnessRacesinOrbit:我应该说“如果&vec
恰好指向包含结构的向量,那么它就是 UB。”更新中...
它在某种程度上被C++ FAQ - Is it legal (and moral) for a member function to say delete this? 所覆盖,其中解释了非静态成员函数可以继续运行没有 UB,即使对象被破坏,所以只要成员函数在对象被删除后不访问它。
@hvd:我不确定我会买那个。引用会更好。
@LightnessRacesinOrbit 是的,它会,但我不认为它是明确允许的,只是隐含的。这是没有任何禁止允许它。发布了一个与此相矛盾的答案。【参考方案3】:
最终,这个问题中有很多不相关的细节。我们可以将其简化为询问以下内容的有效性:
struct A
something_unmovable m;
void operator()()
delete this;
// do something with m
;
并询问这种行为。毕竟resize()
的影响就是调用了对象mid-function-call的析构函数。无论是由std::vector
移动还是复制都无关紧要 - 无论哪种方式,它都会随后被销毁。
标准在 [class.cdtor] 中告诉我们:
对于具有非平凡的对象 析构函数,在析构函数完成后引用对象的任何非静态成员或基类 执行会导致未定义的行为。
因此,如果something_unmovable
的析构函数不平凡(这将使A
的析构函数——或者你的lambda——不平凡),那么在调用析构函数之后对m
的任何引用都是未定义的行为。如果something_unmovable
确实有一个微不足道的析构函数,那么你的代码是完全可以接受的。如果您不在delete this
(您的问题中的resize()
)之后执行任何操作,那么这是完全有效的行为。
m 仍然可以从向量中访问吗?
是的,vec[0]
中的函子仍将包含 m
。它可能是原始 lambda - 也可能是原始 lambda 的副本。但是会有m
一种方式或另一种方式。
【讨论】:
【参考方案4】:函数对象通常是可复制的,因此您的 lambda 将继续运行而不会产生不良影响。如果它通过引用 AFAIR 捕获,内部实现将使用 std::reference_wrapper 以便 lambda 保持可复制。
【讨论】:
我不买这个。可复制的函数对象与它有什么关系? 如果 m 不可移动,它必须是可复制的,否则 lambda 捕获无法捕获它。因此 lambda 是可复制的而不是可移动的。因此,矢量重新分配将按照复制而不是移动进行。因此 lambda 是安全的,因为 m 是一个副本。 不,因为在向量的自动调整大小引发的复制或移动操作期间,正在执行的 lambda 被破坏。这就是问题的重点。 很公平,所以它是 UB 是的,我愿意,并且 OP 要求我们提供证据证明这是否是 UB。你不能只说“是的,它是”。以上是关于如果 lambda 在运行时被移动/破坏会发生啥?的主要内容,如果未能解决你的问题,请参考以下文章
Objective-C多线程,当一个对象在其方法被执行时被释放会发生啥? (以及如何预防?)