[BR] Removing a temporary listener from juce::ListenerList during a callback prevents later listeners being called

Here’s a simple test to repro:

    void runTest() final
    {
        beginTest("Removing listeners during callback");

        struct Listener
        {
            void callback()
            {
                onCallback();
            }

            std::function<void()> onCallback;
        };

        juce::ListenerList<Listener> listeners;
        juce::String result;

        // First listener appends "x" to the result.
        Listener x;
        x.onCallback = [&] {
            result += "x";
        };
        listeners.add(&x);

        // Second listener appends "y" to the result, and
        // adds/removes a temporary listener
        Listener y;
        y.onCallback = [&] {
            result += "y";

            Listener nested;
            listeners.add(&nested);
            listeners.remove(&nested);
        };
        listeners.add(&y);

        // Final listener appends "z" to the result
        Listener z;
        z.onCallback = [&] {
            result += "z";
        };
        listeners.add(&z);

        listeners.call(&Listener::callback);
        expectEquals<juce::String>(result, "xyz");
    }
[juce::ListenerList Test / Removing listeners during callback] Test 1 failed: Expected value: xyz, Actual value: xy

Stepping through juce::ListenerList::callCheckedExcluding(), it seems that the iterator is correctly initialised to {0, 3}, the first iteration executes as expected and the iterator is updated to {1, 3}, however after after the second iteration, the iterator becomes {1, 2} and is then incremented to {2, 2} which causes the loop to end and so the final listener is not called!

It looks like remove() decrements the end of the current iterator to account for existing listeners being removed during callbacks, however add() doesn’t first increment the current iterator to account for listeners being added during callback.

Thanks for reporting there is a fix incoming for this, one question I had however is what is the use case for adding and removing a listener in the same callback? If a listener is added during a callback it is guaranteed not to be called until the next time the listeners are notified, so adding and removing a listener in the same callback means that listener will never be called.

1 Like

It’s a case where the objects in question being listeners is somewhat coincidental.

I have jive::Property, which is similar to juce::Value in that it represents a single property in a juce::ValueTree or a juce::DynamicObject. Properties add themselves as a listener to the given data source on construction, and remove themselves on destruction.

I had a case where, in one property’s callback, I was temporarily creating another property as a convenient way to read another value from the source. E.g. something like:

Property<float> width{ valueTree, "width" };
width.onValueChange = [this] {
    Property<float> height{ valueTree, "height" }; // temp property - listens to the tree
    component.setSize(width, height);    
};

I’ve been able to work around it so not a big deal for me, but wanted to report anyway as it took me quite a lot of debugging to figure out why some of my other callbacks weren’t being called!

Just looking over the commit history, I think Anthony fixed this issue here:

2 Likes