Observer Pattern
- Description: Define a one-to-many dependency so when a subject changes state, all dependents (observers) are notified automatically — the foundation of event-driven and reactive programming.
- My Notion Note ID: K2C-2-18
- Created: 2026-05-22
- Updated: 2026-05-22
- License: Reuse is very welcome. Please credit Yu Zhang and link back to the original on yuzhang.io
Table of Contents
- 1. Intent
- 2. Structure
- 3. C++ Example
- 4. When to Use / When Not To
- 5. Variants and Pitfalls
- 6. Related Patterns
- 7. References
1. Intent
- Publish/subscribe at the object level.
- Subject maintains a list of Observers; on state change, calls each Observer's
update. - Subject doesn't know concrete Observer types — only the interface.
- Foundation of MVC, GUI events, reactive frameworks, signal/slot, event sourcing notifications.
Typical uses:
- GUI — model changes → views redraw.
- Domain events — order placed → inventory + email + analytics react.
- Signals/slots — Qt, boost::signals2.
- Reactive streams — Rx, observable sequences.
- Logging / metrics — multiple sinks subscribe to log events.
2. Structure
Roles:
- Subject — interface for
attach(observer),detach(observer),notify(). Holds observer list. - ConcreteSubject — has state; calls
notify()after mutation; exposesget_state(). - Observer — interface with
update(subject)(or typedupdate(event)). - ConcreteObserver — reacts; may pull state from subject or read pushed event.
ConcreteSubject ──notify()──► [Observer1, Observer2, Observer3]
▲ │
└────── pull state ◄───────────┘ (push model: state passed in update)
Push vs pull:
- Push — subject passes the changed data to
update(event). - Pull — subject only notifies; observers query subject for current state.
- Push = less coupling but rigid event types; pull = more flexible but observers depend on subject's getters.
3. C++ Example
3.1 Classic OO Observer
#include <vector>
#include <algorithm>
#include <memory>
#include <iostream>
#include <string>
class Observer {
public:
virtual ~Observer() = default;
virtual void update(const std::string& event) = 0;
};
class Subject {
std::vector<std::weak_ptr<Observer>> obs_;
public:
void attach(std::shared_ptr<Observer> o) { obs_.push_back(o); }
void detach(const std::shared_ptr<Observer>& o) {
obs_.erase(
std::remove_if(obs_.begin(), obs_.end(),
[&](const std::weak_ptr<Observer>& w) {
return w.expired() || w.lock() == o;
}),
obs_.end());
}
void notify(const std::string& event) {
std::vector<std::shared_ptr<Observer>> live;
for (auto& w : obs_) if (auto s = w.lock()) live.push_back(s);
for (auto& s : live) s->update(event);
}
};
class Logger : public Observer {
public:
void update(const std::string& event) override {
std::cout << "[log] " << event << "\n";
}
};
class Alerter : public Observer {
public:
void update(const std::string& event) override {
if (event.find("ERROR") != std::string::npos) std::cout << "[alert!] " << event << "\n";
}
};
int main() {
Subject s;
auto log = std::make_shared<Logger>();
auto alert = std::make_shared<Alerter>();
s.attach(log);
s.attach(alert);
s.notify("INFO startup");
s.notify("ERROR disk full");
}
weak_ptr lets observers die without the subject holding them alive (Lapsed Listener problem solved).
3.2 boost::signals2 — production-grade signal/slot
For real projects, write Observer with boost::signals2:
#include <boost/signals2.hpp>
#include <iostream>
namespace bs = boost::signals2;
int main() {
bs::signal<void(const std::string&)> sig;
bs::connection c1 = sig.connect([](const std::string& e) {
std::cout << "[log] " << e << "\n";
});
bs::connection c2 = sig.connect([](const std::string& e) {
if (e.find("ERROR") != std::string::npos) std::cout << "[alert!] " << e << "\n";
});
sig("INFO startup");
sig("ERROR disk full");
c1.disconnect();
sig("INFO ignored by logger");
}
signals2 benefits:
- Thread-safe by default — connect/disconnect/emit can be called concurrently.
- Scoped connections (
scoped_connection) auto-disconnect on destruction → RAII subscriptions. - Slot tracking — attach
shared_ptrto a slot; slot auto-disconnects when target expires. - Combiners — combine return values from all slots (default ignores returns).
3.3 Thread-safety pitfall — concurrent subscribe + notify
The naive Observer in § 3.1 is not thread-safe. Two failure modes:
- Concurrent
attach+notify—vectorreallocates while iterated → UB. - Observer detaches itself during
update— modifiesobs_mid-iteration → UB.
Fixes:
- Mutex around
obs_for attach/detach; copy under lock then iterate unlocked (as in § 3.1). - Use
boost::signals2— handles both cases internally. - Use a lock-free queue and defer notifications to a single dispatcher thread.
#include <mutex>
class SafeSubject {
std::vector<std::weak_ptr<Observer>> obs_;
mutable std::mutex m_;
public:
void attach(std::shared_ptr<Observer> o) {
std::lock_guard lk(m_);
obs_.push_back(std::move(o));
}
void notify(const std::string& e) {
std::vector<std::shared_ptr<Observer>> live;
{
std::lock_guard lk(m_);
for (auto& w : obs_) if (auto s = w.lock()) live.push_back(s);
}
for (auto& s : live) s->update(e); // call outside lock — avoid re-entrancy deadlock
}
};
Critical: never call observer callbacks while holding the subject's lock — observer may try to attach/detach → deadlock.
4. When to Use / When Not To
Use when:
- Change in one object requires notifying many others, exact set unknown at compile time.
- Observers can come and go at runtime.
- Subject should not be coupled to observer types.
- Building event-driven, reactive, or pub-sub architectures.
Don't use when:
- One subject, one observer, fixed at compile time → direct call.
- Strict ordering or guaranteed delivery needed → use a real message queue.
- Synchronous notification blocks the subject for too long → switch to async / queue.
- Notification graph becomes a dependency mess — consider Mediator to centralize.
5. Variants and Pitfalls
Variants:
- Push vs pull model — see § 2.
- Typed events —
Observer<Event>template; one observer per event type. - Signal/slot — Qt, boost::signals2, sigslot. Connections as values; RAII unsubscribe.
- Reactive streams — Observer + lazy + composable operators (Rx, ReactiveX, C++20 ranges-like).
- Event sourcing — Observers receive a stream of immutable events.
- Hot vs cold observable — hot: events fire whether or not anyone subscribes. Cold: starts producing on subscription.
Pitfalls:
- Lapsed Listener — observer destroyed without detaching → subject calls a dangling pointer. Fix:
weak_ptr, or RAII connection objects, or signal tracking. - Order of notification — undefined unless documented. Don't rely on insertion order.
- Re-entrant notify — observer mutates subject during update → recursive notify, infinite loop, or invalidation. Defer side effects.
- Concurrent subscribe + notify — see § 3.3. Race or UB without locking; never call slots under the subject's lock.
- Cascading notifications — A's update fires B, B's fires C, C's fires A. Detect cycles or break with a flag.
- Memory churn — fine-grained events → many tiny allocations. Batch, coalesce, or pool.
- Hidden coupling via shared state — observers reading subject state synchronously serialize behavior; mutating subject from inside
updatereintroduces coupling.
6. Related Patterns
- Observer vs Mediator — the canonical contrast:
- Observer = distribute. Subject broadcasts; observers subscribe independently. Peers don't know each other. One-to-many. Notification logic lives at the subject.
- Mediator = centralize. Hub object coordinates components; components talk to the hub. Many-to-many collapses to star. Coordination logic lives in the hub.
- They combine: Mediator is often implemented using Observer internally (Mediator subscribes to events from each colleague, dispatches commands).
- Observer vs Publish-Subscribe (event bus) — Pub-sub is Observer generalized:
- Observer — subject knows the observer list directly.
- Pub-sub — broker / event bus sits between; publisher and subscriber don't reference each other. = Mediator + Observer hybrid.
- Observer vs Command — Observer notifies; Command encapsulates the action to take. Observer's
updatemay instantiate and run Commands. - Observer vs Iterator — Iterator pulls (consumer asks for next), Observer pushes (producer fires). Same dataflow, opposite direction.
- Observer vs Chain of Responsibility — CoR routes one request to one handler in order; Observer fans out one event to many handlers, all run, order undefined.
- Observer vs Memento — orthogonal; Observers may receive Mementos as the payload of a state-change event.
7. References
- Design Patterns (GoF), ch. 5.
- Refactoring.Guru — Observer
- Boost.Signals2
- Qt
QObjectsignals/slots — Observer at framework scale. - ReactiveX — Observer + composable operators (RxCpp, RxJava, RxJS).
- cppreference —
std::weak_ptr