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:
subject.attach(observer)przechowuje wskaźnik.delete observerniszczy obiekt i unieważnia wskaźnik.subject.notify()wywołujeobserver->update()na wiszącym wskaźniku.- 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
-
Preferuj
weak_ptrw ogólnym przypadku. Obserwatorzy mogą znikać niezależnie, a podmiot sam usuwa martwe wpisy.std::vector<std::weak_ptr
> observers_; -
Używaj subskrypcji RAII w ograniczonym zakresie. Wyrejestrowanie następuje, gdy subskrypcja przestaje istnieć.
auto sub = subject.attach(&local_observer);
-
Unikaj surowych wskaźników w nowym kodzie. Sięgaj po inteligentne wskaźniki.
// ❌ Źle subject.attach(&observer);
// ✓ Dobrze subject.attach(std::make_shared
()); -
W Qt zostaw to frameworkowi. System sygnałów i slotów sam pilnuje czasu życia, więc w kodzie Qt to najlepszy wybór.
-
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.