I am currently refactoring an application in which classes can call observers if their state changes. This means that observers are called whenever:
It is this last case that makes me worry.
Suppose that my class is Book. The observers are stored in a class called BookManager (the BookManager also keeps a list of all Books). This means we have this:
class Book
{
...
};
class BookManager
{
private:
std::list<Book *> m_books;
std::list<IObserver *> m_observers;
};
If a book is deleted (removed from the list and deleted from memory), the observers are called:
void BookManager::removeBook (Book *book)
{
m_books.remove(book);
for (auto it=m_observers.cbegin();it!=m_observers.cend();++it) (*it)->onRemove(book *);
delete book;
}
The problem is that I have no control over the logic in the observers. The observers can be delivered by a plugin, by code that is written by developers at the customer.
So, although I can write code like this (and I make sure of getting the next in the list in case the instance is deleted):
auto itNext;
for (auto it=m_books.begin();it!=m_books.end();it=itNext)
{
itNext = it:
++itNext;
Book *book = *it;
if (book->getAuthor()==string("Tolkien"))
{
removeBook(book);
}
}
There is always the possibility of an observer removing other books from the list as well:
void MyObserver::onRemove (Book *book)
{
if (book->getAuthor()==string("Tolkien"))
{
removeAllBooksFromAuthor("Carl Sagan");
}
}
In this case, if the list contains a book of Tolkien, followed by a book of Carl Sagan, the loop that deletes all books of Tolkien will probably crash since the next iterator (itNext) has become invalid.
The illustrated problem can also appear in other situations but the delete-problem is the most severe, since it can easily crash the application.
I could solve the problem by making sure that in the application I first get all instances that I want to delete, put them in a second container, then loop over the second container and delete the instances, but since there is always the risk that an observer explicitly deletes other instances that were already on my to-be-deleted list, I have to put explicit observers to keep this second copy up to date as well.
Also requiring all application code to make copies of lists whenever they want to iterate over a container while calling observers (directly or indirectly) makes writing application code much harder.
Are there [design] patterns that can be used to solve this kind of problem? Preferably no shared-pointer approach since I cannot guarantee that the whole application uses shared-pointers to access the instances.
The basic problem here is that the Book collection gets modified (without knowledge by the application) while the application is iterating over that same collection.
Two ways to deal with that are:
If the delete-loop is part of BookManager
, then it could be restructured like this:
Something similar could be done for other multi-element modifications in BookManager
.