C++ Observer Pattern: Complete Guide | Events, Callbacks & Signal–Slot Patterns

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: DataModel must know all UI components
  • Difficult to expand: DataModel needs to be modified when adding a new component
  • No reuse: Difficult to reuse DataModel in 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

  1. Basic structure
  2. Prevent memory leak with weak_ptr
  3. Observer by event type
  4. Signal/Slot Pattern
  5. Frequently occurring problems and solutions
  6. Production Patterns
  7. 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

conceptDescription
Observer PatternSubject notifies Observer of state change
PurposeLoosely coupled, event-driven architecture
StructureSubject (attach, notify), Observer (update)
AdvantagesScalability, reusability, dynamic subscriptions
DisadvantagesCircular references, uncertain notification order, performance overhead
Use CaseUI 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:

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.


  • 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