What happens if a lambda is moved/destructed while

2020-03-01 06:37发布

问题:

Consider:

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) will probably cause a re-allocation of the vector, which means that the std::functions will be copied to a new location, and the old ones destroyed. Yet this happens while the old one is still running. This particular code runs because the lambda doesn't do anything, but I imagine this can easily result in undefined behavior.

So, what happens exactly? Is m still accessible from the vector? 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?

回答1:

As already covered by other answers, lambdas are essentially syntactic sugar for easily creating types that provide a custom operator() implementation. This is why you can even write lambda invocations using an explicit reference to operator(), like so: int main() { return [](){ return 0; }.operator()(); }. The same rules for all non-static member functions also apply to lambda bodies.

And those rules allow the object being destroyed while the member function is being executed, so long as the member function does not use this afterwards. Your example is an unusual one, the more common example is for a non-static member function executing delete this;. This made it into the C++ FAQ, explaining that it's allowed.

The standard allows this by not really addressing it, as far as I am aware. It describes the semantics of member functions in a way that doesn't rely on the object not being destroyed, so implementations must make sure to let member functions continue executing even if the objects get destroyed.

So to answer your questions:

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?

Yes, pretty much.

Also, is the case where the lambda is movable any different?

No, it's not.

The only time where the lambda being movable could possibly matter is after the lambda has been moved. In your example, the operator() continues executing on the original moved-from and then destroyed functor.



回答2:

You can treat lambda captures like ordinary struct instances.

In your case:

struct lambda_UUID_HERE_stuff
{
    std::vector<std::function<void()>> &vec;
    something_unmovable m;

    void operator()()
    {
        this->vec.resize(100);
    }
};

...and I believe all the same rules apply (as far as VS2013 is concerned).

So, this appears to be another case of undefined behavior. That is, if &vec happens to point to the vector containing the capture instance, and the operations within operator() cause that vector to resize.



回答3:

Ultimately, there are a lot of details in this question that aren't relevant. We can reduce it to asking about the validity of:

struct A {
    something_unmovable m;

    void operator()() {
        delete this;
        // do something with m
    }
};

And ask about that behavior. After all, the impact of the resize() is to call the destructor of the object mid-function-call. Whether it's moved-from or copied-from by std::vector doesn't matter - either way it will subsequently be destroyed.

The standard tells us in [class.cdtor] that:

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.

So if the destructor of something_unmovable is non-trivial (which would make the destructor of A -- or your lambda -- non-trivial), any reference to m after the destructor is called is undefined behavior. If something_unmovable does have a trivial destructor, then your code is perfectly acceptable. If you don't do anything after the delete this (the resize() in your question), then it's perfectly valid behavior.

Is m still accessible from the vector?

Yes, the functor in vec[0] will still have m in it. It may be the original lambda - or it may be a copy of the original lambda. But there will be an m one way or the other.



回答4:

Function objects are normally copyable, so your lambda would continue to run without ill effect. If it captures by reference AFAIR the internal implementation will use std::reference_wrapper so that the lambda remains copyable.