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(); }
Code language: PHP (php)

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.


Les commentaires sont fermés.