Storing lambdas in a juce::Array for later execution


#1

What provoked my question
For some OpenGL based work where some tasks need to be executed on the GL thread, I set up a
juce::Array<std::function<void(juce::OpenGLContext&)>, juce::CriticalSection> executeInRenderCallback
as a class member to which I can add lambdas that need to be executed on the gl thread at any time from any thread and check if there is anything in the array at every OpenGL render callback and then execute this stuff like this:

for (auto &glThreadJob : executeInRenderCallback)
            glThreadJob (openGLContext);

        executeInRenderCallback.clearQuick();

This seemed to work for weeks until I reached an edge case after some recent code additions where there were 9 lambdas to execute - I get a segfault when the first function should be executed. As I think that the array reallocates its storage after having reached a size of 8 elements I strongly suspect this could be the source of that issue.

However, I’m not sure why this is a problem. I always understood std::function as some kind of syntactical nice wrapper around a function pointer (that should be trivially copyable) which is pointing to the lambda function, which itself should be compiled at compiletime and should not be destructed like a real object, so I’m not sure why it should not be valid anymore after resizing the array and with that copying the std::function object? I suspect I could be wrong with this assumption…

My actual question
Am I assuming it right that storing the lambda in an Array is likely to be the source of my bug? If yes, what is the best kind of container to store a various number of lambdas for later execution? Is an OwnedArray the way to go? How do others solve the problem of setting up a queue to execute functions in the GL render thread?


#2

std::function is actually a pretty complex beast with virtual functions, heap allocations (if the small object optimization doesn’t kick in) etc internally. Creating, copying and destroying it is not trivial. If you actually only need function pointers and not the extra captured state that lambdas allow, you might want to consider storing the function pointers into your array directly. Still, I would think storing std::functions into a JUCE Array should not be problematic, but I haven’t myself done that, I’ve only used std::vectors to store std::functions.


#3

Just create a small class containing std::function member, so it will take care of allocation/deallocation.


#4

Array<std::function> is the culprit.

JUCE’s array class uses std::realloc to grow our memory in-place. The advantage to doing this is that in a handful of cases we can beat std::vector for speed, but if you try to realloc memory containing classes which are not TriviallyCopyable then things will go wrong. std::function contains an internal pointer to itself, so when it’s flat-copied into a new location this internal pointer is no longer valid.

We have, however, recently made JUCE’s Array class safer, so if you use the develop branch then you will no longer run into this issue.


#5

That doesn’t seem to make sense, why or how would that help?

This

struct useless
{
  std::function<void(void)> func;
};

Array<useless> funcs;

shouldn’t be any different to just doing :

Array<std::function<void(void)> funcs;

Unless the JUCE Array’s issue is avoided doing things that way…


#6

I completely agree. So much so that I’m preaching the same to everyone at CppCon next week:


#7

Ok, not sure if I got all information spread here right…?

What I got is:

  • std::function is not trivially copyable
  • std::vector<std::function> proved to work
  • recent changes to the Array class will make it work? I‘m not on the tip of the dev branch but also not sooo far behind, maybe a few weeks. So when was the change introduced and will just pulling the last changes make my error disappear?
  • Tom talks about rolling out an own std::function replacement. So is there sich an replacement like that in the current JUCE codebase (which would solve my problem)?

What I definitively didn’t get is for what reason would wrapping the std::function into a stupid object solve the problem?

However thank you for your responses so far :slightly_smiling_face:


#8

It should have been fixed from the 17th July, so more than a few weeks ago… If it works correctly with std::vector but not juce::Array then that’s a bug that needs to be addressed.

There is a replacement in the JUCE codebase, but it’s only used for compatibility in systems which lack a std::function implementation in their standard library. The talk is more focussed on an alternate implementation which provides a compile-time guarantee that it will not allocate, making it more suitable for use on the audio thread.