如果 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::functions 将被复制到一个新位置,而旧的将被销毁。然而,当旧的仍在运行时,就会发生这种情况。这个特定的代码运行是因为 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 而言)。

因此,这似乎是另一种未定义行为的情况。也就是说,如果&amp;vec 恰好指向包含捕获实例的向量,并且operator() 中的操作会导致该向量调整大小。

【讨论】:

这是一个相当空洞的内容,除非你证明它是结构的 UB。 @LightnessRacesinOrbit:我应该说“如果 &amp;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多线程,当一个对象在其方法被执行时被释放会发生啥? (以及如何预防?)

从应用商店更新后,运行 iOS 应用会发生啥?

Prometheus 启动时被禁止的功能特性

循环中的 Lambda 变量捕获 - 这里会发生啥? [复制]

Mongoose 错误:“拓扑被破坏”,有啥问题?

Qt:qthread在关闭期间线程仍在运行时被破坏