The Observer pattern creates lifetime coupling. The subject holds pointers to observers, so deleting an observer while the subject still references it makes the next notification undefined behavior, usually a crash. This page covers the problem and four ways to handle it in modern C++.

The problem

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

delete observer;  // ⚠️ Observer deleted but still in subject's list

subject.notify();  // ❌ Calls deleted observer->update()
                   // Undefined behavior! Likely segfault.

Why this happens:

  1. subject.attach(observer) stores the pointer
  2. delete observer destroys the object, invalidating the pointer
  3. subject.notify() calls observer->update() on a dangling pointer
  4. The pointer may point to freed memory, uninitialized data, or new objects

Solution 1: Shared ownership with std::shared_ptr

Use std::shared_ptr so the observer is deleted only after both the original owner and the subject drop their references.

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

class Observer {
public:
    virtual ~Observer() = default;
    virtual void update(int value) = 0;
};

class Subject {
private:
    std::vector<std::shared_ptr<Observer>> observers_;

public:
    void attach(std::shared_ptr<Observer> observer) {
        observers_.push_back(observer);
    }

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

    void notify(int value) {
        for (auto& observer : observers_) {
            observer->update(value);  // ✓ Safe: shared_ptr keeps object alive
        }
    }
};

// ── Usage ──────────────────────────────────────────────────
class DisplayObserver : public Observer {
public:
    void update(int value) override {
        std::cout << "[Display] " << value << "\n";
    }
};

int main() {
    {
        Subject subject;

        {
            auto observer = std::make_shared<DisplayObserver>();
            subject.attach(observer);

            std::cout << "Observer scope exits...\n";
        }  // observer refcount decremented, but subject still holds a shared_ptr

        std::cout << "Notifying subject...\n";
        subject.notify(42);  // ✓ Safe! Object still alive due to subject's reference
    }

    return 0;
}

Output:

Observer scope exits...
Notifying subject...
[Display] 42

Pros and cons

Pros: - The object is never deleted while in use. - Cleanup is automatic. - Shared ownership is explicit.

Cons: - The observer lives longer than its original scope. - Circular references leak when the subject holds the observer and the observer holds the subject. - Every caller must use std::shared_ptr.

Solution 2: Weak pointers with automatic cleanup

The subject can remove an observer as soon as it is deleted. A std::weak_ptr tells the subject whether the observer still exists.

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

class Observer {
public:
    virtual ~Observer() = default;
    virtual void update(int value) = 0;
};

class Subject {
private:
    // Store weak_ptr; allows observer to be deleted independently
    std::vector<std::weak_ptr<Observer>> observers_;

public:
    void attach(std::shared_ptr<Observer> observer) {
        observers_.push_back(observer);
    }

    void notify(int value) {
        // Remove expired observers and notify live ones
        auto it = observers_.begin();
        while (it != observers_.end()) {
            if (auto observer = it->lock()) {  // Try to upgrade weak_ptr to shared_ptr
                observer->update(value);
                ++it;
            } else {
                // Observer was deleted; remove from list
                it = observers_.erase(it);
            }
        }
    }

    size_t observer_count() const {
        return observers_.size();
    }
};

// ── Usage ──────────────────────────────────────────────────
class DisplayObserver : public Observer {
public:
    void update(int value) override {
        std::cout << "[Display] " << value << "\n";
    }
};

int main() {
    Subject subject;

    {
        auto observer1 = std::make_shared<DisplayObserver>();
        subject.attach(observer1);
        std::cout << "Attached observer1\n";

        {
            auto observer2 = std::make_shared<DisplayObserver>();
            subject.attach(observer2);
            std::cout << "Attached observer2\n";
            std::cout << "Observer count: " << subject.observer_count() << "\n\n";
        }  // observer2 destroyed here

        std::cout << "observer2 scope exited\n";
        std::cout << "Notifying (observer2 auto-removed):\n";
        subject.notify(42);  // observer1 notified; observer2 removed from list

        std::cout << "Observer count: " << subject.observer_count() << "\n";
    }  // observer1 destroyed

    std::cout << "\nNotifying with no observers:\n";
    subject.notify(100);  // No observers to notify

    return 0;
}

Output:

Attached observer1
Attached observer2
Observer count: 2

observer2 scope exited
Notifying (observer2 auto-removed):
[Display] 42
Observer count: 1

Notifying with no observers:

Pros and cons

Pros: - Observers can be deleted independently. - Dead observers are cleaned up automatically. - The observer's lifetime is not extended. - Circular references are not a problem.

Cons: - Weak pointer semantics take more work to follow. - lock() has overhead. - Callers still need std::shared_ptr. - The subject modifies its list while iterating, which can be a problem if observers emit signals.

Solution 3: Scoped registration (RAII)

Return an RAII subscription object that unregisters when it is destroyed. The caller still manages the observer's lifetime, but unregistration happens automatically.

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

class Observer {
public:
    virtual ~Observer() = default;
    virtual void update(int value) = 0;
};

class Subject {
public:
    // Subscription RAII object
    class Subscription {
    private:
        Subject* subject_;
        Observer* observer_;

    public:
        Subscription(Subject* subject, Observer* observer)
            : subject_(subject), observer_(observer) {}

        ~Subscription() {
            if (subject_ && observer_) {
                subject_->detach(observer_);
            }
        }

        // Prevent copying
        Subscription(const Subscription&) = delete;
        Subscription& operator=(const Subscription&) = delete;

        // Allow moving
        Subscription(Subscription&& other) noexcept
            : subject_(other.subject_), observer_(other.observer_) {
            other.subject_ = nullptr;
            other.observer_ = nullptr;
        }
        Subscription& operator=(Subscription&& other) noexcept {
            if (this != &other) {
                // Unsubscribe current
                if (subject_ && observer_) {
                    subject_->detach(observer_);
                }
                subject_ = other.subject_;
                observer_ = other.observer_;
                other.subject_ = nullptr;
                other.observer_ = nullptr;
            }
            return *this;
        }
    };

private:
    std::vector<Observer*> observers_;

public:
    // Returns a subscription RAII object
    Subscription attach(Observer* observer) {
        observers_.push_back(observer);
        return Subscription(this, observer);
    }

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

    void notify(int value) {
        for (auto observer : observers_) {
            observer->update(value);
        }
    }
};

// ── Usage ──────────────────────────────────────────────────
class DisplayObserver : public Observer {
public:
    void update(int value) override {
        std::cout << "[Display] " << value << "\n";
    }
};

int main() {
    Subject subject;
    DisplayObserver observer;

    {
        auto subscription = subject.attach(&observer);
        std::cout << "Subscribed\n";

        subject.notify(42);  // ✓ Observer called
    }  // subscription destroyed; observer automatically unregistered

    std::cout << "Unsubscribed\n";
    subject.notify(100);  // Observer NOT called

    return 0;
}

Output:

Subscribed
[Display] 42
Unsubscribed

Pros and cons

Pros: - No shared_ptr is required, so observers can live on the stack. - The observer unregisters when the subscription goes out of scope. - Subscription lifetime equals registration lifetime, which is easy to reason about. - Callers do not manage observer deletion.

Cons: - Observers must be stack-allocated or managed elsewhere. - The subscription must not outlive the observer. - The code has more boilerplate.

Solution 4: Subject-owned observers

The subject owns its observers and creates them through a factory method.

class Subject {
private:
    std::vector<std::unique_ptr<Observer>> observers_;

public:
    // Create observer owned by subject
    template<typename ObserverType, typename... Args>
    ObserverType* create_observer(Args&&... args) {
        auto observer = std::make_unique<ObserverType>(
            std::forward<Args>(args)...
        );
        auto* ptr = observer.get();
        observers_.push_back(std::move(observer));
        return ptr;
    }

    void notify(int value) {
        for (auto& observer : observers_) {
            observer->update(value);
        }
    }
};

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

    // Observer created by subject; subject owns it
    auto* observer = subject.create_observer<DisplayObserver>();

    subject.notify(42);  // ✓ Safe: subject owns observer

    // Observer automatically deleted when subject destroyed
}

Pros and cons

Pros: - The subject manages lifetime completely. - Subject and observers share a lifetime, so there are no dangling pointers. - No shared_ptr is needed.

Cons: - Observers cannot outlive the subject, which is tight coupling. - Observers cannot exist before or after the subject. - The design does not fit reusable observers such as UI components that observe several subjects.

Comparison

Solution Ownership Complexity Decoupling Use case
shared_ptr Shared Low Good General-purpose, shared ownership
weak_ptr Observer-owned Medium Excellent Observers exist independently
RAII Subscription Observer-owned Medium Good Scoped subscriptions, stack observers
Subject-owned Subject-owned Low Poor Simple, tightly-coupled systems

Best practices

  1. Prefer weak_ptr for general use. Observers can be deleted independently and the subject cleans up after them.

    std::vector<std::weak_ptr> observers_;

  2. Use RAII subscriptions for scoped operations. Unregistration is automatic when the subscription expires.

    auto sub = subject.attach(&local_observer);

  3. Avoid raw pointers in modern code. Use smart pointers instead.

    // ❌ Bad subject.attach(&observer);

    // ✓ Good subject.attach(std::make_shared());

  4. Qt manages this for you. Signals and slots handle lifetime automatically, which makes them the preferred choice in Qt code.

  5. Event systems with callback handles should store handles instead of raw function pointers.

    auto handle = subject.subscribe( { /* ... */ }); // ... handle.unsubscribe(); // Safe removal

Summary

Lifetime management is the part of a manual observer that breaks first. Pick a strategy based on these questions:

  • Do observers outlive the subject?
  • Does the subject own them, the observer, or both?
  • How independent do the two sides need to be?

New C++ code is usually best served by weak_ptr with automatic cleanup or by RAII subscriptions. In Qt code, use Signals/Slots. Subject-owned observers are fine for simple cases when the lifetime relationship is clear.