Search code examples
c++c++11lambdastd-function

Deleting a std::function object within itself


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>();
}

Solution

  • 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.