Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Can I delete a std::function object from within its target function during invocation? [duplicate]

Is this well defined behavior?

#include <functional>

void foo() {
    auto f = new std::function<void()>;
    *f = [f]() { delete f; };
    (*f)();
    f = nullptr;
}

int main() {
    foo();
}

Using the most recent g++, if I do this within a template it causes invalid reads while running under valgrind, otherwise it works fine. Why? Is this a bug in g++?

#include <functional>

template<std::size_t>
void foo() {
    auto f = new std::function<void()>;
    *f = [f]() { delete f; };
    (*f)();
    f = nullptr;
}

int main() {
    foo<0>();
}

2 Answers

This program has well-defined behavior and demonstrates a g++ bug.

The only questionable part of runtime is during the statement (*f)();. The behavior of that line can be picked apart piece by piece. The Standard section numbers below are from N3485; apologies if some don't match C++11.

*f is just the built-in unary operator on a raw pointer to class type. No problem here. The only other evaluation is the function-call expression (*f)(), which invokes void std::function<void()>::operator() const. Then that full-expression is a discarded value.

20.8.11.2.4:

R operator()(ArgTypes... args) const

Effects: INVOKE(obj, std::forward<ArgTypes>(args)..., R) where obj is the target object of *this.

(I've replaced "f" in the Standard with "obj" to reduce confusion with main's f.)

Here obj is a copy of the lambda object, ArgTypes is the empty parameter pack from the specialization std::function<void()>, and R is void.

The INVOKE pseudo-macro is defined in 20.8.2. Since the type of obj is not a pointer-to-member, INVOKE(obj, void) is defined to be obj() implicitly converted to void.

5.1.2p5:

The closure type for a lambda-expression has a public inline function call operator ...

... with exactly described declaration. In this case it turns out to be void operator() const. And its definition is exactly described too:

5.1.2p7:

The lambda-expression's compound-statement yields the function-body of the function call operator, but for purposes of name lookup, determining the type and value of this and transforming id-expressions referring to non-static class members into class member access expressions using (*this), the compound-statement is considered in the context of the lambda-expression.

5.1.2p14:

For each entity captured by copy, an unnamed non-static data member is declared in the closure type.

5.1.2p17:

Every id-expression that is an odr-use of an entity captured by copy is transformed into an access to the corresponding unnamed data member of the closure type.

So the lambda function call operator must be equivalent to:

void __lambda_type::operator() const {
    delete __unnamed_member_f;
}

(where I've invented some names for the unnamed lambda type and unnamed data member.)

The single statement of that call operator is of course equivalent to delete (*this).__unnamed_member_f; So we have:

  • The built-in unary operator* dereference (on the prvalue this)
  • A member access expression
  • A value computation (aka lvalue-to-rvalue conversion) for the member subobject
  • A scalar delete expression
    • Invokes std::function<void()>::~function()
    • Invokes void operator delete(void*)

And finally, in 5.3.5p4:

The cast-expression in a delete-expression shall be evaluated exactly once.

(Here is where g++ is wrong, doing a second value computation on the member subobject between the destructor call and the deallocation function.)

This code cannot cause any other value computations or side effects after the delete expression.

There are some allowances for implementation-defined behavior in lambda types and lambda objects, but none that affect anything above:

5.1.2p3:

An implementation may define the closure type differently from what is described below provided this does not alter the observable behavior of the program other than by changing:

  • the size and/or alignment of the closure type,

  • whether the closure type is trivially copyable,

  • whether the closure type is a standard-layout class, or

  • whether the closure type is a POD class.

like image 141
aschepler Avatar answered Dec 13 '25 02:12

aschepler


It is certainly not well-defined behaviour in general.

Between the end of execution of function object, and the end of the call to operator(), the member operator() is executing on a deleted object. If the implementation reads or writes through this, which it is perfectly allowed to do, then you'll get read or write of a deleted object.

More specifically, the object was only just deleted by this very thread, so it's highly unlikely that any thread actually got around to using it between deletion and the read/write or it was unmapped, so it's quite unlikely to actually cause problems in a simple program. In addition, there's little apparent reason for the implementation to read or write to this after it returns.

However, Valgrind is quite correct that any such read or write would be very invalid and in some circumstances, could lead to random crashes or memory corruption. It's easy to suggest that, between deleting this and a hypothetical read/write, this thread was pre-empted and another thread allocated and used that memory. Alternatively, the memory allocator decided it had enough cached memory of this size and returned this segment to the OS immediately upon freeing it. This is an excellent candidate for a Heisenbug since the conditions to cause it would be relatively rare and only apparent in real complex executing systems rather than trivial test programs.

You could get away with it if you can prove that there are no reads or writes after the function object finishes returning. This basically means guaranteeing the implementation of std::function<Sig>::operator().

Edit:

Mats Peterson's answer raises an interesting question. GCC appears to have implemented the lambda by doing something like this:

struct lambda { std::function<void()>* f; };
void lambda_operator(lambda* l) {
    l->f->~std::function<void()>();
    ::operator delete(l->f);
}

As you can see, the call to operator delete makes a load from l after it's just been deleted, which is exactly the scenario I described above. I'm not actually sure what C++11's memory model rules say about this, I would have thought it was illegal but not necessarily. It might not be defined either way. If it is not illegal, you are definitely screwed.

Clang, however, seems to generate this operator:

void lambda_operator(lambda* l) {
    auto f = l->f;
    f->~std::function<void()>();
    ::operator delete(f);
}

Here when l is deleted it doesn't matter because f was copied into local storage.

To a certain extent, this definitively answers your question- GCC absolutely does load from the memory of the lambda after it's been deleted. Whether or not that's Standard-legal or not, I'm not sure. You could definitely work around this by employing a user-defined function. You'd still have the problem of the std::function implementation issuing loads or stores to this, though.

like image 34
Puppy Avatar answered Dec 13 '25 03:12

Puppy



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!