The Hidden Dangers of std::function and lambdas


Once upon a time, there was an active object which had callbacks using std::function.
On the same day, a user of this object registered a callback function which, in its core, cancelled the registration. That callback was a lambda that aside of that took some reference in the current context.
And guess what happened ?

Here is the code that demonstrates that « fairy tale »:

#include <functional>
#include <iostream>

struct Object {
    using Handler = std::function<void (Object&)>;
    Handler handler;
    void setHandler(Handler f) {
        handler = f;
    }
    void invokeHandler() {
        if (handler) {
            handler(*this);
        }
    }
};

int main()
{
    uint32_t v = 42;
    Object o;
    o.setHandler([&v](Object& obj) {
        std::cout << "before:" << v << std::endl;
        obj.setHandler(nullptr);
        std::cout << "after: " << v << std::endl;
    });
    o.invokeHandler();
}

In its current form, on Linux and on coliru, it doesn’t crash but « after » does not show 42 anymore, only a random number…

Why ?

When the callback invokes setHandler(nullptr), it clears the std::function. As that std::function embedded a copy of the capture of the lambda, clearing the std::function frees the context. As such, the reference to v is now part of a freed block. When the capture is small enough, the corresponding pointer becomes random. When the capture is larger (two uint32_t are sufficient), accessing v in the « after » line is sufficient to cause a segmentation fault.

Hint: valgrind raises an invalid read error. It is unfortunately hard to link it to the copy/clear of the lambda capture in std::function context.

In fact, even when a lambda does capture by reference, the capture is really like a struct containing a reference. Access to the referenced variable is not direct, it always goes through the capture. Such that deleting the std::function deletes the capture and kills the references as well as the copied values.

Share

Les commentaires sont fermés.