1
0
Fork 0
mirror of https://github.com/juce-framework/JUCE.git synced 2026-01-10 23:44:24 +00:00

ListenerList: Prevent calling any listeners that are added during a callback

This fixes an edge case in which if listeners are both removed and added during a callback the added listener(s) may be called during the same iteration
This commit is contained in:
Anthony Nicholls 2024-01-15 15:09:59 +00:00
parent b05b73fb49
commit 67570d34c4
2 changed files with 63 additions and 13 deletions

View file

@ -413,6 +413,34 @@ public:
expect (listeners == nullptr);
expect (TestCriticalSection::numOutOfScopeCalls() == 0);
}
beginTest ("Adding a listener during a callback when one has already been removed");
{
struct Listener{};
ListenerList<Listener> listeners;
expect (listeners.size() == 0);
Listener listener;
listeners.add (&listener);
expect (listeners.size() == 1);
bool listenerCalled = false;
listeners.call ([&] (auto& l)
{
listeners.remove (&l);
expect (listeners.size() == 0);
listeners.add (&l);
expect (listeners.size() == 1);
listenerCalled = true;
});
expect (listenerCalled);
expect (listeners.size() == 1);
}
}
private:

View file

@ -118,9 +118,15 @@ public:
const ScopedLockType lock (listeners->getLock());
if (const auto index = listeners->removeFirstMatchingValue (listenerToRemove); index >= 0)
for (auto* indexPtr : *indices)
if (index <= *indexPtr)
--*indexPtr;
{
for (auto* it : *iterators)
{
--it->end;
if (index <= it->index)
--it->index;
}
}
}
/** Adds a listener that will be automatically removed again when the Guard is destroyed.
@ -146,7 +152,15 @@ public:
If the ListenerList is cleared during a callback, it is guaranteed that
no more listeners will be called.
*/
void clear() { listeners->clear(); }
void clear()
{
const ScopedLockType lock (listeners->getLock());
listeners->clear();
for (auto* it : *iterators)
it->end = 0;
}
/** Returns true if the specified listener has been added to the list. */
bool contains (ListenerClass* listener) const noexcept { return listeners->contains (listener); }
@ -211,20 +225,22 @@ public:
const auto localListeners = listeners;
const ScopedLockType lock { localListeners->getLock() };
int index{};
Iterator it{};
it.end = localListeners->size();
indices->push_back (&index);
const ScopeGuard scope { [i = indices, &index]
iterators->push_back (&it);
const ScopeGuard scope { [i = iterators, &it]
{
i->erase (std::remove (i->begin(), i->end(), &index), i->end());
i->erase (std::remove (i->begin(), i->end(), &it), i->end());
} };
for (const auto end = localListeners->size(); index < jmin (end, localListeners->size()); ++index)
for (; it.index < it.end; ++it.index)
{
if (bailOutChecker.shouldBailOut())
return;
auto* listener = localListeners->getUnchecked (index);
auto* listener = localListeners->getUnchecked (it.index);
if (listener == listenerToExclude)
continue;
@ -313,9 +329,15 @@ private:
using SharedListeners = std::shared_ptr<ArrayType>;
const SharedListeners listeners = std::make_shared<ArrayType>();
using SafeIndices = std::vector<int*>;
using SharedIndices = std::shared_ptr<SafeIndices>;
const SharedIndices indices = std::make_shared<SafeIndices>();
struct Iterator
{
int index{};
int end{};
};
using SafeIterators = std::vector<Iterator*>;
using SharedIterators = std::shared_ptr<SafeIterators>;
const SharedIterators iterators = std::make_shared<SafeIterators>();
//==============================================================================
JUCE_DECLARE_NON_COPYABLE (ListenerList)