SeniorDebuggingOccasionalNot answered yet
Why does a naive GoF Observer become a lifetime and concurrency hazard?
This naive GoF Observer subject works in tests but, in production, crashes when an observer is destroyed without unsubscribing, and misbehaves when a callback subscribes or unsubscribes during notify().
class Subject {
public:
void subscribe(Observer* o) { observers_.push_back(o); }
void unsubscribe(Observer* o) { /* erase-remove ... */ }
void notify() {
for (Observer* o : observers_)
o->update();
}
private:
std::vector<Observer*> observers_;
};
Identify the bug and explain the cause.
The subject holds raw observer pointers it does not own, so a destroyed-but-not-unsubscribed observer becomes a dangling call. Re-entrant notify() — an observer subscribing or destroying itself inside its callback — invalidates the iterator mid-loop. Fix with weak_ptr observers, a copied snapshot of the list before iterating, and deferred add/remove.
- ✗Assuming the observer always outlives the subject — in real code observers are destroyed first and leave a dangling pointer in the list
- ✗Iterating the live observer container directly, so an observer that unsubscribes inside its callback invalidates the iterator
- ✗Locking the subject's mutex across the whole
notify()loop, so an observer callback that calls back into the subject self-deadlocks
- →How does a copied snapshot of the observer list interact with an observer that unsubscribes during notify()?
- →Why is
weak_ptrpreferred over an explicit unsubscribe-in-destructor contract?
Naive GoF Observer — where it breaks
#include <vector>
struct Observer {
virtual ~Observer() = default;
virtual void update() = 0;
};
class Subject {
public:
void subscribe(Observer* o) { observers_.push_back(o); } // raw pointer — subject does not own it
void unsubscribe(Observer* o) { /* erase-remove ... */ }
void notify() {
for (Observer* o : observers_) // ⚠️ iterating the live container
o->update(); // ❌ dangling call if o was already destroyed;
// ❌ if update() calls subscribe()/unsubscribe(),
// the iterator is invalidated mid-loop
}
private:
std::vector<Observer*> observers_; // raw pointers to objects it does not own
};
// ✅ Safe form
#include <algorithm>
#include <memory>
#include <vector>
class SafeSubject {
public:
void subscribe(std::weak_ptr<Observer> o) { observers_.push_back(std::move(o)); }
void notify() {
auto snapshot = observers_; // copied snapshot: survives mutations of the list
for (auto& w : snapshot)
if (auto o = w.lock()) // weak_ptr -> is the observer still alive?
o->update(); // deferred add/remove apply after the loop
std::erase_if(observers_, [](auto& w) { return w.expired(); });
}
private:
std::vector<std::weak_ptr<Observer>> observers_;
};