如果 lambda 是 moved/destructed 而它是 运行 会发生什么?
What happens if a lambda is moved/destructed while it is running?
考虑:
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
将被复制到新位置,而旧的将被销毁。然而,这发生在旧的仍然 运行 的时候。此特定代码运行是因为 lambda 不执行任何操作,但我想这很容易导致未定义的行为。
那么,到底发生了什么? m
是否仍然可以从矢量访问?还是 lambda 的 this
指针现在无效(指向已释放的内存),因此无法访问 lambda 捕获的任何内容,但如果它运行的代码不使用它捕获的任何内容,则它不是未定义的行为?
此外,lambda 可移动的情况有什么不同吗?
您可以像处理普通结构实例一样处理 lambda 捕获。
你的情况:
struct lambda_UUID_HERE_stuff
{
std::vector<std::function<void()>> &vec;
something_unmovable m;
void operator()()
{
this->vec.resize(100);
}
};
...我相信所有相同的规则都适用(就 VS2013 而言)。
所以,这似乎是另一个未定义行为的案例。也就是说,如果 &vec
恰好指向包含捕获实例的向量,并且 operator()
中的操作会导致该向量调整大小。
函数对象通常是可复制的,因此您的 lambda 将继续 运行 而不会产生不良影响。如果它通过引用 AFAIR 捕获,内部实现将使用 std::reference_wrapper 以便 lambda 保持可复制。
正如其他答案已经涵盖的那样,lambda 本质上是语法糖,用于轻松创建提供自定义 operator()
实现的类型。这就是为什么您甚至可以使用对 operator()
的显式引用来编写 lambda 调用,例如:int main() { return [](){ return 0; }.operator()(); }
。所有非静态成员函数的相同规则也适用于 lambda 主体。
并且这些规则允许在执行成员函数时销毁对象,只要成员函数之后不使用 this
。你的例子是一个不寻常的例子,更常见的例子是一个非静态成员函数执行delete this;
。 This made it into the C++ FAQ,解释说这是允许的。
据我所知,该标准通过不真正解决它来允许这一点。它以不依赖对象未被销毁的方式描述成员函数的语义,因此实现必须确保让成员函数继续执行,即使对象被销毁。
所以回答你的问题:
Or is it that the this pointer of the lambda is now invalid (points to freed memory), so nothing the lambda captures can be accessible, yet if it runs code that doesn't use anything it captures, it's not undefined behavior?
是的,差不多。
Also, is the case where the lambda is movable any different?
不,不是。
唯一可能影响 lambda 移动的时间是在移动 lambda 之后。在您的示例中,operator()
继续执行原始移出然后销毁的仿函数。
最终,这个问题中有很多不相关的细节。我们可以将其简化为询问以下内容的有效性:
struct A {
something_unmovable m;
void operator()() {
delete this;
// do something with m
}
};
并询问该行为。毕竟,resize()
的影响是在函数调用中调用对象的析构函数。它是被 std::vector
移动还是复制都无关紧要 - 无论哪种方式,它随后都会被销毁。
标准在 [class.cdtor] 中告诉我们:
For an object with a non-trivial
destructor, referring to any non-static member or base class of the object after the destructor finishes
execution results in undefined behavior.
因此,如果 something_unmovable
的析构函数是非平凡的(这将使 A
的析构函数——或你的 lambda——非平凡的),任何对 [=15= 的引用] 在调用析构函数之后是未定义的行为。如果 something_unmovable
有一个普通的析构函数,那么你的代码是完全可以接受的。如果您 不 在 delete this
(您问题中的 resize()
)之后做任何事情,那么它是完全有效的行为。
Is m still accessible from the vector?
是的,vec[0]
中的仿函数仍将包含 m
。它可能是原始的 lambda,也可能是原始 lambda 的副本。但是会有 m
一种方式或另一种方式。
考虑:
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
将被复制到新位置,而旧的将被销毁。然而,这发生在旧的仍然 运行 的时候。此特定代码运行是因为 lambda 不执行任何操作,但我想这很容易导致未定义的行为。
那么,到底发生了什么? m
是否仍然可以从矢量访问?还是 lambda 的 this
指针现在无效(指向已释放的内存),因此无法访问 lambda 捕获的任何内容,但如果它运行的代码不使用它捕获的任何内容,则它不是未定义的行为?
此外,lambda 可移动的情况有什么不同吗?
您可以像处理普通结构实例一样处理 lambda 捕获。
你的情况:
struct lambda_UUID_HERE_stuff
{
std::vector<std::function<void()>> &vec;
something_unmovable m;
void operator()()
{
this->vec.resize(100);
}
};
...我相信所有相同的规则都适用(就 VS2013 而言)。
所以,这似乎是另一个未定义行为的案例。也就是说,如果 &vec
恰好指向包含捕获实例的向量,并且 operator()
中的操作会导致该向量调整大小。
函数对象通常是可复制的,因此您的 lambda 将继续 运行 而不会产生不良影响。如果它通过引用 AFAIR 捕获,内部实现将使用 std::reference_wrapper 以便 lambda 保持可复制。
正如其他答案已经涵盖的那样,lambda 本质上是语法糖,用于轻松创建提供自定义 operator()
实现的类型。这就是为什么您甚至可以使用对 operator()
的显式引用来编写 lambda 调用,例如:int main() { return [](){ return 0; }.operator()(); }
。所有非静态成员函数的相同规则也适用于 lambda 主体。
并且这些规则允许在执行成员函数时销毁对象,只要成员函数之后不使用 this
。你的例子是一个不寻常的例子,更常见的例子是一个非静态成员函数执行delete this;
。 This made it into the C++ FAQ,解释说这是允许的。
据我所知,该标准通过不真正解决它来允许这一点。它以不依赖对象未被销毁的方式描述成员函数的语义,因此实现必须确保让成员函数继续执行,即使对象被销毁。
所以回答你的问题:
Or is it that the this pointer of the lambda is now invalid (points to freed memory), so nothing the lambda captures can be accessible, yet if it runs code that doesn't use anything it captures, it's not undefined behavior?
是的,差不多。
Also, is the case where the lambda is movable any different?
不,不是。
唯一可能影响 lambda 移动的时间是在移动 lambda 之后。在您的示例中,operator()
继续执行原始移出然后销毁的仿函数。
最终,这个问题中有很多不相关的细节。我们可以将其简化为询问以下内容的有效性:
struct A {
something_unmovable m;
void operator()() {
delete this;
// do something with m
}
};
并询问该行为。毕竟,resize()
的影响是在函数调用中调用对象的析构函数。它是被 std::vector
移动还是复制都无关紧要 - 无论哪种方式,它随后都会被销毁。
标准在 [class.cdtor] 中告诉我们:
For an object with a non-trivial destructor, referring to any non-static member or base class of the object after the destructor finishes execution results in undefined behavior.
因此,如果 something_unmovable
的析构函数是非平凡的(这将使 A
的析构函数——或你的 lambda——非平凡的),任何对 [=15= 的引用] 在调用析构函数之后是未定义的行为。如果 something_unmovable
有一个普通的析构函数,那么你的代码是完全可以接受的。如果您 不 在 delete this
(您问题中的 resize()
)之后做任何事情,那么它是完全有效的行为。
Is m still accessible from the vector?
是的,vec[0]
中的仿函数仍将包含 m
。它可能是原始的 lambda,也可能是原始 lambda 的副本。但是会有 m
一种方式或另一种方式。