본문으로 건너뛰기
Previous
Next
C++ Observer Pattern: Complete Guide | Events

C++ Observer Pattern: Complete Guide | Events

C++ Observer Pattern: Complete Guide | Events

이 글의 핵심

Observer pattern in C++: decouple publishers and subscribers, weak_ptr, typed events, signal/slot style—patterns, pitfalls, and production 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:

  • “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.

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++, Observer Pattern, Design Patterns, Event, Callback, Signal Slot 등으로 검색하시면 이 글이 도움이 됩니다.