This approach pairs Observer with a polymorphic event hierarchy. Instead of encoding each event type in a method signature, you define an Event base class and derive concrete events from it. One subscription mechanism can then handle multiple event types, each with its own data, without adding methods to the observer interface.
Key difference from the traditional approach
The traditional observer implements one method per event:
class Observer {
virtual void on_value_changed(int value) = 0;
virtual void on_state_changed(State state) = 0;
virtual void on_error_occurred(std::string error) = 0;
};
// Every observer must implement all three methods,
// even if it only cares about one event type.
The event-based system implements a single on_event() and dispatches inside:
class EventListener {
virtual void on_event(const Event& event) = 0;
};
class ValueChangedEvent : public Event { /* ... */ };
class StateChangedEvent : public Event { /* ... */ };
class ErrorOccurredEvent : public Event { /* ... */ };
// Observer receives all events; uses RTTI or visitor to dispatch
listener->on_event(ValueChangedEvent{10});
listener->on_event(StateChangedEvent{State::READY});
Adding an event type does not change the observer interface, and each observer can handle only the events it cares about.
The code
#include <iostream>
#include <vector>
#include <memory>
#include <typeinfo>
#include <string>
#include <sstream>
// ── Base Event class ──────────────────────────────────────
class Event {
public:
virtual ~Event() = default;
virtual std::string type_name() const = 0;
virtual void describe() const = 0;
};
// ── Concrete Event types ──────────────────────────────────
class ValueChangedEvent : public Event {
public:
int old_value;
int new_value;
ValueChangedEvent(int old_val, int new_val)
: old_value(old_val), new_value(new_val) {}
std::string type_name() const override {
return "ValueChangedEvent";
}
void describe() const override {
std::cout << " [Event] Value changed: " << old_value
<< " → " << new_value << "\n";
}
};
class StateChangedEvent : public Event {
public:
enum class State { IDLE, LOADING, READY, ERROR };
State old_state;
State new_state;
StateChangedEvent(State old, State new_st)
: old_state(old), new_state(new_st) {}
std::string type_name() const override {
return "StateChangedEvent";
}
void describe() const override {
auto state_name = [](State s) {
switch(s) {
case State::IDLE: return "IDLE";
case State::LOADING: return "LOADING";
case State::READY: return "READY";
case State::ERROR: return "ERROR";
}
return "?";
};
std::cout << " [Event] State changed: " << state_name(old_state)
<< " → " << state_name(new_state) << "\n";
}
};
class ErrorOccurredEvent : public Event {
public:
std::string message;
explicit ErrorOccurredEvent(const std::string& msg)
: message(msg) {}
std::string type_name() const override {
return "ErrorOccurredEvent";
}
void describe() const override {
std::cout << " [Event] Error: " << message << "\n";
}
};
// ── Event Listener interface ──────────────────────────────
class EventListener {
public:
virtual ~EventListener() = default;
virtual void on_event(const Event& event) = 0;
};
// ── Concrete Listeners ────────────────────────────────────
// Listener that cares about ALL events
class LoggingListener : public EventListener {
public:
void on_event(const Event& event) override {
std::cout << "[Logger] Received " << event.type_name() << ":\n";
event.describe();
}
};
// Listener that only cares about value changes
class DisplayListener : public EventListener {
public:
void on_event(const Event& event) override {
// RTTI approach: check type at runtime
if (const auto* value_event = dynamic_cast<const ValueChangedEvent*>(&event)) {
std::cout << "[Display] New value: " << value_event->new_value << "\n";
}
// Ignore other events
}
};
// Listener that only cares about errors
class AlertListener : public EventListener {
public:
void on_event(const Event& event) override {
if (const auto* error_event = dynamic_cast<const ErrorOccurredEvent*>(&event)) {
std::cout << "[Alert] " << error_event->message << "\n";
}
}
};
// Listener with selective interest
class StateMonitor : public EventListener {
private:
int value_changes_count_ = 0;
int errors_count_ = 0;
public:
void on_event(const Event& event) override {
if (const auto* value_event = dynamic_cast<const ValueChangedEvent*>(&event)) {
value_changes_count_++;
} else if (const auto* error_event = dynamic_cast<const ErrorOccurredEvent*>(&event)) {
errors_count_++;
std::cout << "[Monitor] Error #" << errors_count_ << ": "
<< error_event->message << "\n";
}
}
void report() const {
std::cout << "\n[Monitor] Summary:\n";
std::cout << " - Value changes: " << value_changes_count_ << "\n";
std::cout << " - Errors: " << errors_count_ << "\n";
}
};
// ── Event Emitter ─────────────────────────────────────────
class EventSystem {
private:
std::vector<std::unique_ptr<EventListener>> listeners_;
public:
void subscribe(std::unique_ptr<EventListener> listener) {
listeners_.push_back(std::move(listener));
}
void emit(const Event& event) {
for (auto& listener : listeners_) {
listener->on_event(event);
}
}
size_t listener_count() const { return listeners_.size(); }
};
// ── Subject that emits events ─────────────────────────────
class DataModel {
private:
int value_ = 0;
bool is_loaded_ = false;
EventSystem& event_system_;
public:
explicit DataModel(EventSystem& events)
: event_system_(events) {}
void set_value(int new_value) {
if (value_ != new_value) {
ValueChangedEvent event(value_, new_value);
event_system_.emit(event);
value_ = new_value;
}
}
void load() {
event_system_.emit(StateChangedEvent(
StateChangedEvent::State::IDLE,
StateChangedEvent::State::LOADING
));
// Simulate loading...
if (true) { // Success
event_system_.emit(StateChangedEvent(
StateChangedEvent::State::LOADING,
StateChangedEvent::State::READY
));
is_loaded_ = true;
} else { // Failure
event_system_.emit(StateChangedEvent(
StateChangedEvent::State::LOADING,
StateChangedEvent::State::ERROR
));
event_system_.emit(ErrorOccurredEvent("Load failed"));
}
}
};
// ── Usage ──────────────────────────────────────────────────
int main() {
EventSystem events;
// Subscribe different listeners
events.subscribe(std::make_unique<LoggingListener>());
events.subscribe(std::make_unique<DisplayListener>());
events.subscribe(std::make_unique<AlertListener>());
auto* monitor = new StateMonitor();
events.subscribe(std::unique_ptr<EventListener>(monitor));
std::cout << "Connected " << events.listener_count() << " listeners\n\n";
DataModel model(events);
std::cout << "Setting value to 42:\n";
model.set_value(42);
std::cout << "\nLoading data:\n";
model.load();
std::cout << "\nSetting value to 100:\n";
model.set_value(100);
std::cout << "\nTriggering error:\n";
events.emit(ErrorOccurredEvent("Database connection failed"));
monitor->report();
return 0;
}
Output:
Connected 4 listeners
Setting value to 42:
[Logger] Received ValueChangedEvent:
[Event] Value changed: 0 → 42
[Display] New value: 42
Loading data:
[Logger] Received StateChangedEvent:
[Event] State changed: IDLE → LOADING
[Logger] Received StateChangedEvent:
[Event] State changed: LOADING → READY
[Display] New value: 42
Setting value to 100:
[Logger] Received ValueChangedEvent:
[Event] Value changed: 42 → 100
[Display] New value: 100
Triggering error:
[Logger] Received ErrorOccurredEvent:
[Event] Error: Database connection failed
[Alert] Database connection failed
[Monitor] Error #1: Database connection failed
[Monitor] Summary:
- Value changes: 2
- Errors: 1
Design variants
1. Visitor pattern for event dispatch
Instead of dynamic_cast, use the Visitor pattern for type-safe dispatch:
class EventVisitor {
public:
virtual ~EventVisitor() = default;
virtual void visit(ValueChangedEvent&) = 0;
virtual void visit(StateChangedEvent&) = 0;
virtual void visit(ErrorOccurredEvent&) = 0;
};
class Event {
public:
virtual void accept(EventVisitor& visitor) = 0;
};
class ValueChangedEvent : public Event {
public:
void accept(EventVisitor& v) override { v.visit(*this); }
// ...
};
// In listener:
class DisplayListener : public EventListener, public EventVisitor {
public:
void on_event(const Event& event) override {
const_cast<Event&>(event).accept(*this);
}
void visit(ValueChangedEvent& e) override {
std::cout << "[Display] " << e.new_value << "\n";
}
void visit(StateChangedEvent&) override { /* ignore */ }
void visit(ErrorOccurredEvent&) override { /* ignore */ }
};
2. Template-based typed events
To get type safety without RTTI:
template<typename EventType>
class TypedEventSystem {
private:
std::vector<std::function<void(const EventType&)>> callbacks_;
public:
void subscribe(std::function<void(const EventType&)> cb) {
callbacks_.push_back(cb);
}
void emit(const EventType& event) {
for (auto& cb : callbacks_) {
cb(event);
}
}
};
// Usage:
TypedEventSystem<ValueChangedEvent> value_system;
value_system.subscribe([](const ValueChangedEvent& e) {
std::cout << "Value: " << e.new_value << "\n";
});
Advantages
- Events form a natural inheritance tree.
- Listeners handle only the events they care about.
- You can add an event type without changing the listener interface.
- Event names say what happened (
ValueChangedEvent, notupdate()). - One subscription mechanism covers many notification types.
Disadvantages
dynamic_castcosts time at runtime, and the Visitor alternative adds complexity.- The call chain
Event->accept->listener->visitadds indirection. - You still need smart pointers for safety (see the next page).
- Each notification type needs its own event class.
dynamic_castbypasses compile-time checking, though type-safe alternatives exist.
When to use
- Complex systems with many event types
- Listeners that care about a subset of events
- Long-lived systems where event types change often
- You want event names that describe what happened
- Integration with systems that expect event hierarchies
Comparison
| Aspect | Traditional | Function-Based | Qt Signals | Event System |
|---|---|---|---|---|
| Event types | One per signal | Implicit in signature | One per signal | Polymorphic hierarchy |
| Listener scope | Must implement all | Per-callback | All slots receive | Selective handling |
| Adding events | Modify interface | Add signal | Modify interface | Inherit from Event |
| Type safety | Compile-time | Lost | Compile-time | Runtime (w/ RTTI) |
| Complexity | Low | Medium | Medium | High |
Next steps
- Lifetime management covers lifetime management and ways to avoid dangling pointers.