The traditional Observer pattern uses virtual functions and an abstract base class. The subject keeps a list of observer pointers and calls their update() method when state changes.

Structure

Mermaid diagram

The code

#include <iostream>
#include <vector>
#include <memory>
#include <algorithm>

// ── Observer interface ─────────────────────────────────────
class Observer {
public:
    virtual ~Observer() = default;
    virtual void update(int new_value) = 0;
};

// ── Subject (Publisher) ────────────────────────────────────
class Subject {
private:
    int state_ = 0;
    std::vector<Observer*> observers_;

public:
    virtual ~Subject() = default;

    void attach(Observer* observer) {
        observers_.push_back(observer);
    }

    void detach(Observer* observer) {
        auto it = std::find(observers_.begin(), observers_.end(), observer);
        if (it != observers_.end()) {
            observers_.erase(it);
        }
    }

    void set_state(int new_state) {
        if (state_ != new_state) {
            state_ = new_state;
            notify();
        }
    }

    int get_state() const { return state_; }

private:
    void notify() {
        for (auto observer : observers_) {
            observer->update(state_);
        }
    }
};

// ── Concrete observers ─────────────────────────────────────
class DisplayObserver : public Observer {
public:
    void update(int new_value) override {
        std::cout << "[Display] State changed to: " << new_value << "\n";
    }
};

class LoggingObserver : public Observer {
public:
    void update(int new_value) override {
        std::cout << "[Logger] Recording state change: " << new_value << "\n";
    }
};

class AlertObserver : public Observer {
private:
    int threshold_;

public:
    explicit AlertObserver(int threshold) : threshold_(threshold) {}

    void update(int new_value) override {
        if (new_value > threshold_) {
            std::cout << "[Alert] WARNING: State " << new_value 
                      << " exceeds threshold " << threshold_ << "\n";
        }
    }
};

// ── Usage ──────────────────────────────────────────────────
int main() {
    Subject subject;

    DisplayObserver display;
    LoggingObserver logger;
    AlertObserver alert(50);

    // Register observers
    subject.attach(&display);
    subject.attach(&logger);
    subject.attach(&alert);

    std::cout << "Setting state to 10:\n";
    subject.set_state(10);

    std::cout << "\nSetting state to 60:\n";
    subject.set_state(60);

    std::cout << "\nDetaching logger, setting state to 20:\n";
    subject.detach(&logger);
    subject.set_state(20);

    return 0;
}

Output:

Setting state to 10:
[Display] State changed to: 10
[Logger] Recording state change: 10

Setting state to 60:
[Display] State changed to: 60
[Logger] Recording state change: 60
[Alert] WARNING: State 60 exceeds threshold 50

Detaching logger, setting state to 20:
[Display] State changed to: 20

Advantages

  • Virtual functions make the intent clear.
  • Each observer can keep its own internal state.
  • You can overload update() for different notification types.
  • Observers can take different constructor parameters, as AlertObserver(threshold) does.
  • You control exactly when and how notification happens.

Disadvantages

  • Raw pointers mean manual lifetime management. If an observer is deleted while attached, the subject keeps a dangling pointer.
  • All observers share one interface, so type information is lost.
  • You get no control over notification order.
  • Each observer costs a vtable pointer.
  • Adding a parameter to update() means changing every observer implementation.

When to use

  • You have a stable, known set of observer types.
  • Observers need constructor parameters or internal state.
  • You want explicit control over the notification process.
  • Your framework supports polymorphism but not templates, as in pure C or old codebases.

Lifetime issues

If you delete an observer while it is still attached, the subject keeps a dangling pointer:

Subject subject;
DisplayObserver* observer = new DisplayObserver();
subject.attach(observer);

delete observer;  // ❌ Observer is now deleted but still in subject's list

subject.set_state(10);  // ❌ Calls deleted observer's update() → undefined behavior!

See Lifetime management for lifetime management strategies with RAII, smart pointers, and weak_ptr.

Next steps