C++ Observer Pattern: Complete Guide | Events, Callbacks & Signal–Slot Patterns
이 글의 핵심
Hands-on Observer pattern in C++: loose coupling, memory-safe subscriptions, priorities, async notify, and stock-ticker style examples.
Behavioral patterns—including Observer—are surveyed in C++ behavioral patterns #20-1 and overview #20-2. For event/listener-heavy APIs elsewhere, compare JavaScript patterns.
What is the Observer pattern? Why use it?
Problem Scenario: State Change Notification
Problem: When the data model changes, multiple UI components need to be updated. Directly calling each component creates strong coupling.
// Bad example: strong coupling
class DataModel {
public:
void setValue(int v) {
value = v;
// Call UI component directly
chart->update(value);
label->update(value);
logger->log(value);
}
private:
int value;
Chart* chart;
Label* label;
Logger* logger;
};
Problem:
- strong coupling:
DataModelmust know all UI components - Difficult to expand:
DataModelneeds to be modified when adding a new component - No reuse: Difficult to reuse
DataModelin other projects
Solution: Observer Pattern separates Subject and Observer. Subject only manages the Observer list and notifies with notify() when state changes.
// Good example: loose coupling
class DataModel {
public:
void setValue(int v) {
value = v;
notify(value); // Notify all Observers
}
void attach(std::shared_ptr<Observer> obs) {
observers.push_back(obs);
}
private:
int value;
std::vector<std::weak_ptr<Observer>> observers;
void notify(int value) {
for (auto& obs : observers) {
if (auto ptr = obs.lock()) {
ptr->update(value);
}
}
}
};
flowchart TD
subject["Subject (DataModel)"]
obs1["Observer 1 (Chart)"]
obs2["Observer 2 (Label)"]
obs3["Observer 3 (Logger)"]
subject -->|notify| obs1
subject -->|notify| obs2
subject -->|notify| obs3
obs1 -.->|attach| subject
obs2 -.->|attach| subject
obs3 -.->|attach| subject
index
- Basic structure
- Prevent memory leak with weak_ptr
- Observer by event type
- Signal/Slot Pattern
- Frequently occurring problems and solutions
- Production Patterns
- Complete Example: Stock Market Monitor
1. basic structure
Minimum Observer
#include <iostream>
#include <vector>
#include <memory>
class Observer {
public:
virtual void update(int value) = 0;
virtual ~Observer() = default;
};
class Subject {
public:
void attach(std::shared_ptr<Observer> obs) {
observers.push_back(obs);
}
void notify(int value) {
for (auto& obs : observers) {
obs->update(value);
}
}
private:
std::vector<std::shared_ptr<Observer>> observers;
};
class ConcreteObserver : public Observer {
public:
ConcreteObserver(const std::string& name) : name_(name) {}
void update(int value) override {
std::cout << name_ << " received: " << value << '\n';
}
private:
std::string name_;
};
int main() {
Subject subject;
auto obs1 = std::make_shared<ConcreteObserver>("Observer1");
auto obs2 = std::make_shared<ConcreteObserver>("Observer2");
subject.attach(obs1);
subject.attach(obs2);
subject.notify(42);
// Observer1 received: 42
// Observer2 received: 42
}
2. Prevent memory leaks with weak_ptr
Problem: Circular references
// ❌ Misuse: Circular reference with shared_ptr
class Subject {
std::vector<std::shared_ptr<Observer>> observers; // strong reference
};
// Subject owns Observer, Observer owns Subject → circular reference
Solved: weak_ptr
#include <iostream>
#include <vector>
#include <memory>
class Observer {
public:
virtual void update(int value) = 0;
virtual ~Observer() = default;
};
class Subject {
public:
void attach(std::shared_ptr<Observer> obs) {
observers.push_back(obs); // Save as weak_ptr
}
void notify(int value) {
// Remove expired Observer
observers.erase(
std::remove_if(observers.begin(), observers.end(),
{
return wp.expired();
}),
observers.end()
);
// alarm
for (auto& obs : observers) {
if (auto ptr = obs.lock()) {
ptr->update(value);
}
}
}
private:
std::vector<std::weak_ptr<Observer>> observers;
};
class ConcreteObserver : public Observer {
public:
ConcreteObserver(const std::string& name) : name_(name) {}
~ConcreteObserver() {
std::cout << name_ << " destroyed\n";
}
void update(int value) override {
std::cout << name_ << " received: " << value << '\n';
}
private:
std::string name_;
};
int main() {
Subject subject;
{
auto obs1 = std::make_shared<ConcreteObserver>("Observer1");
subject.attach(obs1);
subject.notify(42); // Observer1 received: 42
} // destroy obs1
subject.notify(100); // Expired Observer will not be notified
}
output of power:
Observer1 received: 42
Observer1 destroyed
3. Observer by event type
Various event handling
#include <iostream>
#include <vector>
#include <memory>
#include <string>
class Event {
public:
virtual ~Event() = default;
};
class ValueChangedEvent : public Event {
public:
ValueChangedEvent(int v) : value(v) {}
int value;
};
class ErrorEvent : public Event {
public:
ErrorEvent(const std::string& m) : message(m) {}
std::string message;
};
class Observer {
public:
virtual void onEvent(const Event& event) = 0;
virtual ~Observer() = default;
};
class Subject {
public:
void attach(std::shared_ptr<Observer> obs) {
observers.push_back(obs);
}
void notifyEvent(const Event& event) {
for (auto& obs : observers) {
if (auto ptr = obs.lock()) {
ptr->onEvent(event);
}
}
}
private:
std::vector<std::weak_ptr<Observer>> observers;
};
class ConcreteObserver : public Observer {
public:
void onEvent(const Event& event) override {
if (auto* ve = dynamic_cast<const ValueChangedEvent*>(&event)) {
std::cout << "Value changed: " << ve->value << '\n';
} else if (auto* ee = dynamic_cast<const ErrorEvent*>(&event)) {
std::cout << "Error: " << ee->message << '\n';
}
}
};
int main() {
Subject subject;
auto obs = std::make_shared<ConcreteObserver>();
subject.attach(obs);
subject.notifyEvent(ValueChangedEvent(42));
subject.notifyEvent(ErrorEvent("Something went wrong"));
}
4. Signal/Slot Pattern
Qt-style signals/slots
#include <iostream>
#include <vector>
#include <functional>
template<typename... Args>
class Signal {
public:
using Slot = std::function<void(Args...)>;
void connect(Slot slot) {
slots.push_back(slot);
}
void emit(Args... args) {
for (auto& slot : slots) {
slot(args...);
}
}
private:
std::vector<Slot> slots;
};
class Button {
public:
Signal<> clicked;
void click() {
std::cout << "Button clicked\n";
clicked.emit();
}
};
int main() {
Button button;
button.clicked.connect( {
std::cout << "Handler 1: Button was clicked\n";
});
button.clicked.connect( {
std::cout << "Handler 2: Logging click event\n";
});
button.click();
// Button clicked
// Handler 1: Button was clicked
// Handler 2: Logging click event
}
5. Frequently occurring problems and solutions
Problem 1: Circular references
Symptom: Memory leak.
Cause: Subject and Observer refer to each other as shared_ptr.
// ❌ Misuse: Circular reference with shared_ptr
class Subject {
std::vector<std::shared_ptr<Observer>> observers;
};
// ✅ Correct usage: weak_ptr
class Subject {
std::vector<std::weak_ptr<Observer>> observers;
};
Issue 2: Observer removal during notification
Symptom: Iterator invalidation, crash.
Cause: Observer calls detach() during notify().
// ❌ Incorrect use: Removed during notification
void notify() {
for (auto& obs : observers) {
obs->update(); // call detach() in update() → invalidate iterator
}
}
// ✅ Correct use: Notify with copy
void notify() {
auto copy = observers; // copy
for (auto& obs : copy) {
if (auto ptr = obs.lock()) {
ptr->update();
}
}
}
Problem 3: Reentry
Symptom: Infinite loop.
Cause: Calling Subject’s setValue() in Observer’s update() → notify() again.
// ❌ Misuse: Reentrancy
void Observer::update(int value) {
subject->setValue(value + 1); // infinite loop
}
// ✅ Correct use: Prevent re-entrancy
class Subject {
void notify() {
if (notifying) return; // Prevent re-entry
notifying = true;
// ... alarm ...
notifying = false;
}
private:
bool notifying = false;
};
6. production pattern
Pattern 1: Priority Observer
#include <map>
#include <memory>
class Subject {
public:
void attach(std::shared_ptr<Observer> obs, int priority = 0) {
observers[priority].push_back(obs);
}
void notify(int value) {
// Notifications in order of highest priority
for (auto it = observers.rbegin(); it != observers.rend(); ++it) {
for (auto& obs : it->second) {
if (auto ptr = obs.lock()) {
ptr->update(value);
}
}
}
}
private:
std::map<int, std::vector<std::weak_ptr<Observer>>> observers;
};
Pattern 2: Asynchronous notification
#include <thread>
#include <future>
class Subject {
public:
void notifyAsync(int value) {
auto copy = observers;
std::thread([copy, value]() {
for (auto& obs : copy) {
if (auto ptr = obs.lock()) {
ptr->update(value);
}
}
}).detach();
}
private:
std::vector<std::weak_ptr<Observer>> observers;
};
7. Complete Example: Stock Market Monitor
#include <iostream>
#include <vector>
#include <memory>
#include <string>
#include <algorithm>
class StockObserver {
public:
virtual void onPriceChanged(const std::string& symbol, double price) = 0;
virtual ~StockObserver() = default;
};
class StockMarket {
public:
void attach(std::shared_ptr<StockObserver> obs) {
observers.push_back(obs);
}
void detach(std::shared_ptr<StockObserver> obs) {
observers.erase(
std::remove_if(observers.begin(), observers.end(),
[&obs](const std::weak_ptr<StockObserver>& wp) {
auto sp = wp.lock();
return !sp || sp == obs;
}),
observers.end()
);
}
void setPrice(const std::string& symbol, double price) {
prices[symbol] = price;
notifyPriceChanged(symbol, price);
}
private:
std::map<std::string, double> prices;
std::vector<std::weak_ptr<StockObserver>> observers;
void notifyPriceChanged(const std::string& symbol, double price) {
auto copy = observers;
for (auto& obs : copy) {
if (auto ptr = obs.lock()) {
ptr->onPriceChanged(symbol, price);
}
}
}
};
class PriceDisplay : public StockObserver {
public:
PriceDisplay(const std::string& name) : name_(name) {}
void onPriceChanged(const std::string& symbol, double price) override {
std::cout << "[" << name_ << "] " << symbol << ": $" << price << '\n';
}
private:
std::string name_;
};
class PriceAlert : public StockObserver {
public:
PriceAlert(const std::string& symbol, double threshold)
: symbol_(symbol), threshold_(threshold) {}
void onPriceChanged(const std::string& symbol, double price) override {
if (symbol == symbol_ && price > threshold_) {
std::cout << "ALERT: " << symbol << " exceeded $" << threshold_ << '\n';
}
}
private:
std::string symbol_;
double threshold_;
};
int main() {
StockMarket market;
auto display = std::make_shared<PriceDisplay>("MainDisplay");
auto alert = std::make_shared<PriceAlert>("AAPL", 150.0);
market.attach(display);
market.attach(alert);
market.setPrice("AAPL", 145.0); // [MainDisplay] AAPL: $145
market.setPrice("AAPL", 155.0); // [MainDisplay] AAPL: $155
// ALERT: AAPL exceeded $150
}
organize
| concept | Description |
|---|---|
| Observer Pattern | Subject notifies Observer of state change |
| Purpose | Loosely coupled, event-driven architecture |
| Structure | Subject (attach, notify), Observer (update) |
| Advantages | Scalability, reusability, dynamic subscriptions |
| Disadvantages | Circular references, uncertain notification order, performance overhead |
| Use Case | UI updates, event system, MVC patterns |
Observer Pattern is a core design pattern that implements loose coupling in event-based systems.
FAQ
Q1: When do I use the Observer Pattern?
A: Used when changes in the state of one object need to be notified to multiple objects and loose coupling is required.
Q2: Why do you use weak_ptr?
A: Used to prevent circular references and automatically remove Observers.
Q3: Is the notification order guaranteed?
A: Not guaranteed. If you need priority, use the Priority Observer pattern.
Q4: What is the difference from signal/slot?
A: Signal/Slot is a variant of the Observer Pattern that is type safe and connects function objects directly (Qt style).
Q5: What is the performance overhead?
A: Proportional to the number of Observers. This can be alleviated with asynchronous notifications.
Q6: What are the Observer Pattern learning resources?
A:
- “Design Patterns” by Gang of Four
- “Head First Design Patterns” by Freeman & Freeman
- Refactoring Guru: Observer Pattern
One-line summary: Observer Pattern allows you to implement event-driven architecture and achieve loose coupling. Next, it would be a good idea to read Strategy Pattern.
Good article to read together (internal link)
Here’s another article related to this topic.
- C++ weak_ptr | “Weak Pointers” Guide
- C++ Factory Pattern Complete Guide | Object creation encapsulation and extensibility
Practical tips
These are tips that can be applied right away in practice.
Debugging tips
- If you run into a problem, check the compiler warnings first.
- Reproduce the problem with a simple test case
Performance Tips
- Don’t optimize without profiling
- Set measurable indicators first
Code review tips
- Check in advance for areas that are frequently pointed out in code reviews.
- Follow your team’s coding conventions
Practical checklist
This is what you need to check when applying this concept in practice.
Before writing code
- Is this technique the best way to solve the current problem?
- Can team members understand and maintain this code?
- Does it meet the performance requirements?
Writing code
- Have you resolved all compiler warnings?
- Have you considered edge cases?
- Is error handling appropriate?
When reviewing code
- Is the intent of the code clear?
- Are there enough test cases?
- Is it documented?
Use this checklist to reduce mistakes and improve code quality.
Keywords covered in this article (related search terms)
This article will be helpful if you search for C++, observer, pattern, event, callback, signal-slot, etc.
Related articles
- C++ Adapter Pattern Complete Guide | Interface conversion and compatibility
- Complete Guide to C++ Command Pattern | Undo and macro system
- C++ CRTP Complete Guide | Static polymorphism and compile-time optimization
- C++ Decorator Pattern Complete Guide | Dynamic addition and combination of functions
- C++ Factory Pattern Complete Guide | Object creation encapsulation and extensibility