The Observer pattern creates lifetime coupling. The subject holds pointers to observers, so deleting an observer while the subject still references it makes the next notification undefined behavior, usually a crash. This page covers the problem and four ways to handle it in modern C++.
The problem
Subject subject;
Observer* observer = new DisplayObserver();
subject.attach(observer);
delete observer; // ⚠️ Observer deleted but still in subject's list
subject.notify(); // ❌ Calls deleted observer->update()
// Undefined behavior! Likely segfault.
Why this happens:
subject.attach(observer)stores the pointerdelete observerdestroys the object, invalidating the pointersubject.notify()callsobserver->update()on a dangling pointer- The pointer may point to freed memory, uninitialized data, or new objects
Solution 1: Shared ownership with std::shared_ptr
Use std::shared_ptr so the observer is deleted only after both the original owner and the subject drop their references.
#include <memory>
#include <vector>
#include <iostream>
class Observer {
public:
virtual ~Observer() = default;
virtual void update(int value) = 0;
};
class Subject {
private:
std::vector<std::shared_ptr<Observer>> observers_;
public:
void attach(std::shared_ptr<Observer> observer) {
observers_.push_back(observer);
}
void detach(std::shared_ptr<Observer> observer) {
auto it = std::find(observers_.begin(), observers_.end(), observer);
if (it != observers_.end()) {
observers_.erase(it);
}
}
void notify(int value) {
for (auto& observer : observers_) {
observer->update(value); // ✓ Safe: shared_ptr keeps object alive
}
}
};
// ── Usage ──────────────────────────────────────────────────
class DisplayObserver : public Observer {
public:
void update(int value) override {
std::cout << "[Display] " << value << "\n";
}
};
int main() {
{
Subject subject;
{
auto observer = std::make_shared<DisplayObserver>();
subject.attach(observer);
std::cout << "Observer scope exits...\n";
} // observer refcount decremented, but subject still holds a shared_ptr
std::cout << "Notifying subject...\n";
subject.notify(42); // ✓ Safe! Object still alive due to subject's reference
}
return 0;
}
Output:
Observer scope exits...
Notifying subject...
[Display] 42
Pros and cons
Pros: - The object is never deleted while in use. - Cleanup is automatic. - Shared ownership is explicit.
Cons:
- The observer lives longer than its original scope.
- Circular references leak when the subject holds the observer and the observer holds the subject.
- Every caller must use std::shared_ptr.
Solution 2: Weak pointers with automatic cleanup
The subject can remove an observer as soon as it is deleted. A std::weak_ptr tells the subject whether the observer still exists.
#include <memory>
#include <vector>
#include <iostream>
class Observer {
public:
virtual ~Observer() = default;
virtual void update(int value) = 0;
};
class Subject {
private:
// Store weak_ptr; allows observer to be deleted independently
std::vector<std::weak_ptr<Observer>> observers_;
public:
void attach(std::shared_ptr<Observer> observer) {
observers_.push_back(observer);
}
void notify(int value) {
// Remove expired observers and notify live ones
auto it = observers_.begin();
while (it != observers_.end()) {
if (auto observer = it->lock()) { // Try to upgrade weak_ptr to shared_ptr
observer->update(value);
++it;
} else {
// Observer was deleted; remove from list
it = observers_.erase(it);
}
}
}
size_t observer_count() const {
return observers_.size();
}
};
// ── Usage ──────────────────────────────────────────────────
class DisplayObserver : public Observer {
public:
void update(int value) override {
std::cout << "[Display] " << value << "\n";
}
};
int main() {
Subject subject;
{
auto observer1 = std::make_shared<DisplayObserver>();
subject.attach(observer1);
std::cout << "Attached observer1\n";
{
auto observer2 = std::make_shared<DisplayObserver>();
subject.attach(observer2);
std::cout << "Attached observer2\n";
std::cout << "Observer count: " << subject.observer_count() << "\n\n";
} // observer2 destroyed here
std::cout << "observer2 scope exited\n";
std::cout << "Notifying (observer2 auto-removed):\n";
subject.notify(42); // observer1 notified; observer2 removed from list
std::cout << "Observer count: " << subject.observer_count() << "\n";
} // observer1 destroyed
std::cout << "\nNotifying with no observers:\n";
subject.notify(100); // No observers to notify
return 0;
}
Output:
Attached observer1
Attached observer2
Observer count: 2
observer2 scope exited
Notifying (observer2 auto-removed):
[Display] 42
Observer count: 1
Notifying with no observers:
Pros and cons
Pros: - Observers can be deleted independently. - Dead observers are cleaned up automatically. - The observer's lifetime is not extended. - Circular references are not a problem.
Cons:
- Weak pointer semantics take more work to follow.
- lock() has overhead.
- Callers still need std::shared_ptr.
- The subject modifies its list while iterating, which can be a problem if observers emit signals.
Solution 3: Scoped registration (RAII)
Return an RAII subscription object that unregisters when it is destroyed. The caller still manages the observer's lifetime, but unregistration happens automatically.
#include <memory>
#include <vector>
#include <iostream>
class Observer {
public:
virtual ~Observer() = default;
virtual void update(int value) = 0;
};
class Subject {
public:
// Subscription RAII object
class Subscription {
private:
Subject* subject_;
Observer* observer_;
public:
Subscription(Subject* subject, Observer* observer)
: subject_(subject), observer_(observer) {}
~Subscription() {
if (subject_ && observer_) {
subject_->detach(observer_);
}
}
// Prevent copying
Subscription(const Subscription&) = delete;
Subscription& operator=(const Subscription&) = delete;
// Allow moving
Subscription(Subscription&& other) noexcept
: subject_(other.subject_), observer_(other.observer_) {
other.subject_ = nullptr;
other.observer_ = nullptr;
}
Subscription& operator=(Subscription&& other) noexcept {
if (this != &other) {
// Unsubscribe current
if (subject_ && observer_) {
subject_->detach(observer_);
}
subject_ = other.subject_;
observer_ = other.observer_;
other.subject_ = nullptr;
other.observer_ = nullptr;
}
return *this;
}
};
private:
std::vector<Observer*> observers_;
public:
// Returns a subscription RAII object
Subscription attach(Observer* observer) {
observers_.push_back(observer);
return Subscription(this, observer);
}
void detach(Observer* observer) {
auto it = std::find(observers_.begin(), observers_.end(), observer);
if (it != observers_.end()) {
observers_.erase(it);
}
}
void notify(int value) {
for (auto observer : observers_) {
observer->update(value);
}
}
};
// ── Usage ──────────────────────────────────────────────────
class DisplayObserver : public Observer {
public:
void update(int value) override {
std::cout << "[Display] " << value << "\n";
}
};
int main() {
Subject subject;
DisplayObserver observer;
{
auto subscription = subject.attach(&observer);
std::cout << "Subscribed\n";
subject.notify(42); // ✓ Observer called
} // subscription destroyed; observer automatically unregistered
std::cout << "Unsubscribed\n";
subject.notify(100); // Observer NOT called
return 0;
}
Output:
Subscribed
[Display] 42
Unsubscribed
Pros and cons
Pros:
- No shared_ptr is required, so observers can live on the stack.
- The observer unregisters when the subscription goes out of scope.
- Subscription lifetime equals registration lifetime, which is easy to reason about.
- Callers do not manage observer deletion.
Cons: - Observers must be stack-allocated or managed elsewhere. - The subscription must not outlive the observer. - The code has more boilerplate.
Solution 4: Subject-owned observers
The subject owns its observers and creates them through a factory method.
class Subject {
private:
std::vector<std::unique_ptr<Observer>> observers_;
public:
// Create observer owned by subject
template<typename ObserverType, typename... Args>
ObserverType* create_observer(Args&&... args) {
auto observer = std::make_unique<ObserverType>(
std::forward<Args>(args)...
);
auto* ptr = observer.get();
observers_.push_back(std::move(observer));
return ptr;
}
void notify(int value) {
for (auto& observer : observers_) {
observer->update(value);
}
}
};
// ── Usage ──────────────────────────────────────────────────
int main() {
Subject subject;
// Observer created by subject; subject owns it
auto* observer = subject.create_observer<DisplayObserver>();
subject.notify(42); // ✓ Safe: subject owns observer
// Observer automatically deleted when subject destroyed
}
Pros and cons
Pros:
- The subject manages lifetime completely.
- Subject and observers share a lifetime, so there are no dangling pointers.
- No shared_ptr is needed.
Cons: - Observers cannot outlive the subject, which is tight coupling. - Observers cannot exist before or after the subject. - The design does not fit reusable observers such as UI components that observe several subjects.
Comparison
| Solution | Ownership | Complexity | Decoupling | Use case |
|---|---|---|---|---|
| shared_ptr | Shared | Low | Good | General-purpose, shared ownership |
| weak_ptr | Observer-owned | Medium | Excellent | Observers exist independently |
| RAII Subscription | Observer-owned | Medium | Good | Scoped subscriptions, stack observers |
| Subject-owned | Subject-owned | Low | Poor | Simple, tightly-coupled systems |
Best practices
-
Prefer
weak_ptrfor general use. Observers can be deleted independently and the subject cleans up after them.std::vector<std::weak_ptr
> observers_; -
Use RAII subscriptions for scoped operations. Unregistration is automatic when the subscription expires.
auto sub = subject.attach(&local_observer);
-
Avoid raw pointers in modern code. Use smart pointers instead.
// ❌ Bad subject.attach(&observer);
// ✓ Good subject.attach(std::make_shared
()); -
Qt manages this for you. Signals and slots handle lifetime automatically, which makes them the preferred choice in Qt code.
-
Event systems with callback handles should store handles instead of raw function pointers.
auto handle = subject.subscribe( { /* ... */ }); // ... handle.unsubscribe(); // Safe removal
Summary
Lifetime management is the part of a manual observer that breaks first. Pick a strategy based on these questions:
- Do observers outlive the subject?
- Does the subject own them, the observer, or both?
- How independent do the two sides need to be?
New C++ code is usually best served by weak_ptr with automatic cleanup or by RAII subscriptions. In Qt code, use Signals/Slots. Subject-owned observers are fine for simple cases when the lifetime relationship is clear.