federico barbieri
federico barbieri

Reputation: 39

Calling std::function destructor while still executing it

I want to dynamically change the behaviour of a method of a class, so I implemented these method calling the operator() of a std::function holding a copy of one lambda function, that depends on some values known only after the class construction, at a time. The lambdas change the state of the class, so they reset a container holding the behaviours of all dynamic methods.
Executing the above idea I was not able to access the capture list of the lamba after resetting the container.
The following snippet reproduces the problem:

std::vector< std::function<void(std::string)> > vector;

int main() {

   //Change class state when variable value will be known
   std::string variableValue = "hello";

   auto function = [variableValue](std::string arg) {
     std::cout <<"From capture list, before: "<< variableValue << std::endl;
     std::cout <<"From arg,          before: " << arg << std::endl;

     vector.clear();

     std::cout << "From capture list, after: " << variableValue << std::endl;
     std::cout << "From arg,          after: " << arg << std::endl;
   };

   vector.push_back(function);

   //Dynamic method execution
   vector[0](variableValue);

   return 0;
}

Producing output:

From capture list, before: hello
From arg,          before: hello
From capture list, after:
From arg,          after: hello

where variableValue is invalidated after vector was clean.

Is the capture list invalidation an expected result? Is safe using any other local variable, not only in the capture list, after calling std::function destructor? Is there a suggested way / pattern to accomplish the same behaviour in a safer way (excluding huge switches/if on class states)?

Upvotes: 3

Views: 774

Answers (2)

Lightness Races in Orbit
Lightness Races in Orbit

Reputation: 385174

We can get rid of the std::function, lambda and vector for this question. Since lambdas are just syntactic sugar for classes with a function-call operator, your testcase is effectively the same as this:

struct Foo
{
   std::string variableValue = "hello";

   void bar(std::string arg)
   {
      std::cout <<"From capture list, before: "<< variableValue << std::endl;
      std::cout <<"From arg,          before: " << arg << std::endl;

      delete this;  // ugrh

      std::cout << "From capture list, after: " << variableValue << std::endl;
      std::cout << "From arg,          after: " << arg << std::endl;
   }
};


int main()
{
    Foo* ptr = new Foo();
    ptr->bar(variableValue);
}

The function argument is fine because it's a copy, but after delete this the member Foo::variableValue no longer exists, so your program has undefined behaviour from trying to use it.

Common wisdom is that continuing to run the function itself is legal (because function definitions aren't objects and cannot be "deleted"; they are just a fundamental property of your program), as long as you leave the encapsulating class's members well enough alone.

I would, however, advise avoiding this pattern unless you really need it. It'll be easy to confuse people as to the ownership responsibilities of your class (even when "your class" is autonomously-generated from a lambda expression!).


Is the capture list invalidation an expected result?

Yes.

Is safe using any other local variable, not only in the capture list, after calling std::function destructor?

Yes.

Is there a suggested way / pattern to accomplish the same behaviour in a safer way (excluding huge switches/if on class states)?

That's impossible to say for sure without understanding what it is that you're trying to do. But you could try playing around with storing shared_ptrs in your vector instead… Just be careful not to capture a shared_ptr in the lambda itself, or it'll never be cleaned up! Capturing a weak_ptr instead can be good for this; it can be "converted" to a shared_ptr inside the lambda body, which will protect the lambda's life for the duration of said body.

Upvotes: 3

Del
Del

Reputation: 1309

std::function's destructor destroys the object's target if the object is non-empty, where the target is the wrapped callable object.

In your case, the target is a lambda expression. When you use a lambda expression, the compiler generates a "non-union non-aggregate class type" that contains the captures-by-value as data members and has operator() as a member function.

When you execute vector.clear(), the destructors of its elements are run, and therefore the destructors of the closure's captures-by-value, which are member variables, are run.

As for captures-by-reference, "the reference variable's lifetime ends when the lifetime of the closure object ends."

So, it is not safe to access any capture, whether by value and by reference, after std::function's destructor runs.

What about the actual operator()? "Functions are not objects," so they don't have lifetimes. So, the mere execution of the operator() after the destructor has been run should be fine, as long as you don't access any captures. See the conditions under which one can safely delete this.

Upvotes: 0

Related Questions