Wzorzec Observer wiąże czas życia podmiotu i obserwatorów. Podmiot trzyma wskaźniki do obserwatorów, więc jeśli obserwator zniknie, a podmiot nadal się do niego odwołuje, następne powiadomienie wywoła niezdefiniowane zachowanie, najprawdopodobniej crash. Ta strona pokazuje problem i sposoby jego rozwiązania w nowoczesnym C++.

Problem

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

delete observer;  // ⚠️ Obserwator usunięty, ale wciąż na liście podmiotu

subject.notify();  // ❌ Wywołuje deleted observer->update()
                   // Niezdefiniowane zachowanie! Prawdopodobny segfault.

Dlaczego się to dzieje:

  1. subject.attach(observer) przechowuje wskaźnik.
  2. delete observer niszczy obiekt i unieważnia wskaźnik.
  3. subject.notify() wywołuje observer->update() na wiszącym wskaźniku.
  4. Wskaźnik może wskazywać na zwolnioną pamięć, niezainicjalizowane dane lub nowe obiekty.

Rozwiązanie 1: wspólna własność z std::shared_ptr

Użyj std::shared_ptr, żeby obserwator zniknął dopiero wtedy, gdy swoje odwołania zwolnią zarówno oryginalny właściciel, jak i podmiot.

#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);  // ✓ Bezpieczne: shared_ptr utrzymuje obiekt przy życiu
        }
    }
};

// ── Użycie ────────────────────────────────────────────────
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 << "Zakres obserwatora kończy się...\n";
        }  // refcount obserwatora zmniejszony, ale podmiot wciąż trzyma shared_ptr

        std::cout << "Powiadamianie podmiotu...\n";
        subject.notify(42);  // ✓ Bezpieczne! Obiekt wciąż żyje dzięki odwołaniu podmiotu
    }

    return 0;
}

Wyjście

Zakres obserwatora kończy się...
Powiadamianie podmiotu...
[Display] 42

Zalety i wady

Zalety - Obiekt nie zostanie usunięty, dopóki ktoś go używa. - Sprzątanie odbywa się automatycznie. - Wspólna własność jest jawna.

Wady - Czas życia obserwatora wychodzi poza zamierzony zakres. - Cykliczne odwołania mogą powodować wycieki: podmiot trzyma obserwatora, a obserwator podmiot. - Wszystkie obiekty wywoływalne muszą używać std::shared_ptr.

Rozwiązanie 2: słabe wskaźniki z automatycznym oczyszczaniem

Gdy obserwator zniknie, usuń go też z listy podmiotu. std::weak_ptr pozwala wykryć, że obiekt już nie istnieje.

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

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

class Subject {
private:
    // Przechowuj weak_ptr; pozwala obserwatorowi być usunięty niezależnie
    std::vector<std::weak_ptr<Observer>> observers_;

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

    void notify(int value) {
        // Usuń wygasłych obserwatorów i powiadom żywych
        auto it = observers_.begin();
        while (it != observers_.end()) {
            if (auto observer = it->lock()) {  // Spróbuj uaktualnić weak_ptr na shared_ptr
                observer->update(value);
                ++it;
            } else {
                // Obserwator został usunięty; usuń z listy
                it = observers_.erase(it);
            }
        }
    }

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

// ── Użycie ────────────────────────────────────────────────
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 << "Dołączono observer1\n";

        {
            auto observer2 = std::make_shared<DisplayObserver>();
            subject.attach(observer2);
            std::cout << "Dołączono observer2\n";
            std::cout << "Liczba obserwatorów: " << subject.observer_count() << "\n\n";
        }  // observer2 niszczony tutaj

        std::cout << "Zakres observer2 wychodzący\n";
        std::cout << "Powiadamianie (observer2 auto-usunięty):\n";
        subject.notify(42);  // observer1 powiadomiony; observer2 usunięty z listy

        std::cout << "Liczba obserwatorów: " << subject.observer_count() << "\n";
    }  // observer1 niszczony

    std::cout << "\nPowiadamianie bez obserwatorów:\n";
    subject.notify(100);  // Brak obserwatorów do powiadomienia

    return 0;
}

Wyjście

Dołączono observer1
Dołączono observer2
Liczba obserwatorów: 2

Zakres observer2 wychodzący
Powiadamianie (observer2 auto-usunięty):
[Display] 42
Liczba obserwatorów: 1

Powiadamianie bez obserwatorów:

Zalety i wady

Zalety - Obserwatorzy mogą znikać niezależnie od siebie. - Martwe obserwatory są usuwane automatycznie. - Czas życia obserwatora nie wychodzi poza zamierzony zakres. - Brak problemów z cyklicznymi odwołaniami.

Wady - Bardziej złożony kod (semantyka weak_ptr). - lock() kosztuje. - Obiekty wywoływalne nadal potrzebują std::shared_ptr. - Podmiot zmienia listę w trakcie iteracji, co może sprawić kłopot, jeśli obserwatorzy emitują sygnały.

Rozwiązanie 3: rejestracja o ograniczonym zakresie (RAII)

Zwróć obiekt RAII (subskrypcję), który wyrejestruje się przy zniszczeniu. Czasem życia obserwatora zarządza obiekt wywoływalny, a wyrejestrowanie dzieje się samo.

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

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

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

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

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

        // Zapobiegaj kopiowaniu
        Subscription(const Subscription&) = delete;
        Subscription& operator=(const Subscription&) = delete;

        // Zezwól na przenoszenie
        Subscription(Subscription&& other) noexcept
            : subject_(other.subject_), observer_(other.observer_) {
            other.subject_ = nullptr;
            other.observer_ = nullptr;
        }
        Subscription& operator=(Subscription&& other) noexcept {
            if (this != &other) {
                // Wyrejestruj bieżący
                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:
    // Zwraca obiekt subskrypcji RAII
    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);
        }
    }
};

// ── Użycie ────────────────────────────────────────────────
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 << "Zasubskrybowano\n";

        subject.notify(42);  // ✓ Obserwator wywoływany
    }  // subskrypcja niszczona; obserwator automatycznie wyrejestrowany

    std::cout << "Wyrejestrowano\n";
    subject.notify(100);  // Obserwator NIEWYWOŁYWANY

    return 0;
}

Wyjście

Zasubskrybowano
[Display] 42
Wyrejestrowano

Zalety i wady

Zalety - Nie trzeba shared_ptr, obserwator może leżeć na stosie. - Subskrypcja wyrejestrowuje się sama, gdy wychodzi z zakresu. - Jasny czas życia: subskrypcja żyje tyle, ile rejestracja. - Obiekty wywoływalne nie muszą pilnować usunięcia obserwatora.

Wady - Obserwator musi być na stosie albo zarządzany z zewnątrz. - Subskrypcja nie może przeżyć obserwatora. - Więcej kodu szablonowego.

Rozwiązanie 4: obserwatorzy należący do podmiotu

Podmiot jest właścicielem obserwatorów i zarządza ich czasem życia. Sam ich tworzy.

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

public:
    // Utwórz obserwatora należącego do podmiotu
    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);
        }
    }
};

// ── Użycie ────────────────────────────────────────────────
int main() {
    Subject subject;

    // Obserwator utworzony przez podmiot; podmiot go posiada
    auto* observer = subject.create_observer<DisplayObserver>();

    subject.notify(42);  // ✓ Bezpieczne: podmiot posiada obserwatora

    // Obserwator automatycznie usunięty, gdy podmiot jest niszczony
}

Zalety i wady

Zalety - Najprostsze rozwiązanie: cały czas życia jest w rękach podmiotu. - Brak wiszących wskaźników, bo podmiot i obserwatorzy żyją tyle samo. - Nie trzeba shared_ptr.

Wady - Ścisłe sprzężenie: obserwator nie może przeżyć podmiotu. - Obserwator nie może istnieć przed podmiotem ani po nim. - Nie nadaje się do obserwatorów używanych wielokrotnie, np. komponentów interfejsu, które obserwują kilka podmiotów.

Porównanie

Rozwiązanie Własność Złożoność Rozdzielenie Przypadek użycia
shared_ptr Wspólna Niska Dobre Ogólne zastosowanie, wspólna własność
weak_ptr Należy do obserwatora Średnia Dobre Obserwatorzy istnieją niezależnie
RAII Subscription Należy do obserwatora Średnia Dobre Subskrypcje o ograniczonym zakresie, obserwator na stosie
Subject-owned Należy do podmiotu Niska Słabe Proste, ściśle sprzężone systemy

Najlepsze praktyki

  1. Preferuj weak_ptr w ogólnym przypadku. Obserwatorzy mogą znikać niezależnie, a podmiot sam usuwa martwe wpisy.

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

  2. Używaj subskrypcji RAII w ograniczonym zakresie. Wyrejestrowanie następuje, gdy subskrypcja przestaje istnieć.

    auto sub = subject.attach(&local_observer);

  3. Unikaj surowych wskaźników w nowym kodzie. Sięgaj po inteligentne wskaźniki.

    // ❌ Źle subject.attach(&observer);

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

  4. W Qt zostaw to frameworkowi. System sygnałów i slotów sam pilnuje czasu życia, więc w kodzie Qt to najlepszy wybór.

  5. W systemach zdarzeń używaj uchwytów callbacków. Zamiast surowych wskaźników do funkcji trzymaj uchwyty.

    auto handle = subject.subscribe( { /* ... */ }); // ... handle.unsubscribe(); // Bezpieczne usunięcie

Podsumowanie

W ręcznym Observerze pierwsze psuje się zarządzanie czasem życia. Strategię wybierz na podstawie:

  • Czasu życia obserwatora. Czy ma przeżyć podmiot?
  • Modelu własności. Wspólna, należy do obserwatora czy do podmiotu?
  • Wymaganego rozdzielenia. Jak niezależni mają być obserwatorzy?

W nowym kodzie C++ wybieraj weak_ptr z automatycznym czyszczeniem albo subskrypcje RAII. W istniejącym kodzie Qt używaj sygnałów i slotów, bo robią to same. W prostych przypadkach obserwatorzy należący do podmiotu są w porządku, jeśli relacja czasu życia jest jasna.