Zamiast zmuszać obserwatorów do dziedziczenia z abstrakcyjnej klasy bazowej, można trzymać obiekty wywoływalne w std::function: funkcje, lambdy albo związane metody. Dziedziczenie znika, a jednolinijkowy callback zajmuje jedną linijkę.
Główna różnica wobec podejścia tradycyjnego
Oparte na interfejsie. Obserwatorzy dziedziczą z Observer i nadpisują update().
class MyObserver : public Observer { /* ... */ };
Oparte na funkcjach. Połączyć się może każdy obiekt wywoływalny zgodny z sygnaturą.
subject.subscribe([](int value) { std::cout << value; });
subject.subscribe(&some_function);
subject.subscribe(&object, &Class::member_function);
Struktura
Subject {
std::vector<std::function<void(int)>> callbacks;
// Połącz każdy obiekt wywoływalny pasujący do sygnatury
void subscribe(std::function<void(int)> callback)
// Wywołaj wszystkich subskrybentów
void notify(int value)
}
Kod
#include <iostream>
#include <vector>
#include <functional>
#include <algorithm>
// ── Subject (Wydawca) ──────────────────────────────────────
template<typename EventType>
class Subject {
private:
std::vector<std::function<void(EventType)>> callbacks_;
public:
// Subskrybuj z obiektem wywoływalnym (funkcja, lambda, funktor)
void subscribe(std::function<void(EventType)> callback) {
callbacks_.push_back(callback);
}
// Wysubskrybuj porównując wskaźniki funkcji (działa dla prostych przypadków)
// Dla lamd i złożonych obiektów wywoływalnych, przechowuj uchwyty zamiast tego (patrz poniżej)
void notify(EventType event) {
for (auto& callback : callbacks_) {
callback(event);
}
}
size_t observer_count() const { return callbacks_.size(); }
};
// ── Samodzielne funkcje callback ───────────────────────────
void display_callback(int value) {
std::cout << "[Display] Wartość: " << value << "\n";
}
void logging_callback(int value) {
std::cout << "[Logger] Wartość: " << value << "\n";
}
// ── Klasa obserwatora z callback'iem członkowskim ──────────
class AlertSystem {
private:
int threshold_;
public:
explicit AlertSystem(int threshold) : threshold_(threshold) {}
void on_value_changed(int value) {
if (value > threshold_) {
std::cout << "[Alert] OSTRZEŻENIE: " << value
<< " przekracza " << threshold_ << "\n";
}
}
};
// ── Użycie z różnymi typami obiektów wywoływalnych ──────────
int main() {
Subject<int> subject;
// 1. Funkcja wolna
subject.subscribe(display_callback);
// 2. Inna funkcja wolna
subject.subscribe(logging_callback);
// 3. Funkcja członkowska (używając std::bind)
AlertSystem alert(50);
subject.subscribe(std::bind(&AlertSystem::on_value_changed, &alert, std::placeholders::_1));
// 4. Lambda (wbudowana, bez oddzielnej funkcji)
subject.subscribe([](int value) {
std::cout << "[Custom] Obserwator lambda: " << value << "\n";
});
std::cout << "Połączono " << subject.observer_count() << " obserwatorów\n\n";
std::cout << "Powiadamianie z wartością 10:\n";
subject.notify(10);
std::cout << "\nPowiadamianie z wartością 60:\n";
subject.notify(60);
return 0;
}
Wyjście
Połączono 4 obserwatorów
Powiadamianie z wartością 10:
[Display] Wartość: 10
[Logger] Wartość: 10
[Custom] Obserwator lambda: 10
Powiadamianie z wartością 60:
[Display] Wartość: 60
[Logger] Wartość: 60
[Alert] OSTRZEŻENIE: 60 przekracza 50
[Custom] Obserwator lambda: 60
Zarządzanie subskrypcjami za pomocą uchwytów
Największa wada powyższego podejścia to brak wygodnego sposobu na anulowanie subskrypcji. Lambdy nie da się łatwo usunąć, bo jest anonimowa. Rozwiązaniem jest uchwyt subskrypcji:
template<typename EventType>
class Subject {
public:
using Callback = std::function<void(EventType)>;
class SubscriptionHandle {
private:
std::vector<Callback>* callbacks_;
size_t index_;
friend class Subject;
SubscriptionHandle(std::vector<Callback>* callbacks, size_t index)
: callbacks_(callbacks), index_(index) {}
public:
void unsubscribe() {
if (callbacks_ && index_ < callbacks_->size()) {
callbacks_->erase(callbacks_->begin() + index_);
}
}
};
SubscriptionHandle subscribe(Callback callback) {
callbacks_.push_back(callback);
return SubscriptionHandle(&callbacks_, callbacks_.size() - 1);
}
void notify(EventType event) {
for (auto& callback : callbacks_) {
callback(event);
}
}
private:
std::vector<Callback> callbacks_;
};
// ── Użycie ────────────────────────────────────────────────
int main() {
Subject<int> subject;
auto handle = subject.subscribe([](int v) {
std::cout << "Lambda: " << v << "\n";
});
subject.notify(10); // Lambda wywoływana
handle.unsubscribe(); // Rozłącz
subject.notify(20); // Lambda NIEWYWOŁYWANA
}
Type erasure przy wielu typach zdarzeń
Tradycyjne podejście daje obserwatorowi jedną sygnaturę update. Przy std::function każdy typ zdarzenia wymaga osobnej subskrypcji:
class EventSystem {
public:
std::vector<std::function<void(int)>> on_int_event;
std::vector<std::function<void(std::string)>> on_string_event;
std::vector<std::function<void()>> on_simple_event;
void subscribe_int(std::function<void(int)> cb) {
on_int_event.push_back(cb);
}
void subscribe_string(std::function<void(std::string)> cb) {
on_string_event.push_back(cb);
}
// ... itd
};
To działa, ale robi się przydługie. Czystsze podejście do wielu typów zdarzeń z zachowaniem bezpieczeństwa typów opisuje strona Zorientowany obiektowo system zdarzeń.
Zalety
- Bez dziedziczenia. Obserwatorzy nie potrzebują wspólnej klasy bazowej.
- Dowolne obiekty wywoływalne. Działają lambdy, wskaźniki do funkcji, metody i funktory.
- Krótszy kod. Nie trzeba pisać osobnej klasy dla każdego obserwatora.
- Łatwiejsze testy. Lambdę przekazujesz bezpośrednio, bez klas mocków.
- Procedury obsługi w miejscu użycia. Możesz je zdefiniować tam, gdzie są używane.
Wady
- Type erasure. Wszystkie obiekty wywoływalne trafiają do
std::function, więc typ znika na etapie kompilacji. - Wydajność. Dochodzi pośredniość przez wskaźnik do funkcji i możliwy narzut small-object optimization.
- Trudne anulowanie subskrypcji. Anonimowej lambdy nie usuniesz bez uchwytu.
- Brak stanu obserwatora. W przeciwieństwie do obserwatorów opartych na klasach lambda ma dostęp tylko do tego, co przechwyci.
- Nieokreślona kolejność. Tak jak w tradycyjnym Observerze, kolejność powiadomień jest nieokreślona.
Kiedy stosować
- Masz wiele prostych źródeł zdarzeń (przyciski, suwaki) i krótkie procedury obsługi.
- Procedury obsługi to jednolinijkowce albo krótkie lambdy.
- Nie chcesz tworzyć klas obserwatorów dla trywialnych callbacków.
- Integrujesz się z systemami zdarzeń, które oczekują sygnatur
std::function.
Porównanie z innymi podejściami
| Aspekt | Oparte na interfejsie | Oparte na funkcjach | Qt Signals/Slots | System zdarzeń |
|---|---|---|---|---|
| Dziedziczenie wymagane | Tak | Nie | Nie (makra) | Czasami |
| Type safety | Częściowy | Tracona (type erased) | Pełny (kompilacja) | Pełny |
| Lambdy | Niezręcznie | Naturalne | Naturalne | Naturalne |
| Anulowanie subskrypcji | Porównanie wskaźnika | Potrzeba uchwytów | Metoda disconnect | Potrzeba uchwytów |
| Wydajność | Szybka (bezpośredni vtable) | Wolniejsza (type erasure) | Bardzo szybka (kompilacja) | Szybka (wirtualna) |
| Framework | Standardowy C++ | Standardowy C++ | Specyficzny dla Qt | Standardowy C++ |
Następne kroki
- Sygnały i sloty Qt. Mechanizm sygnałów i slotów, bezpieczny typowo i sprawdzany w kompilacji.
- Zorientowany obiektowo system zdarzeń. Rozszerzalny, polimorficzny system obiektów zdarzeń do złożonych scenariuszy.
- Zarządzanie czasem życia. Zarządzanie czasem życia obserwatora i callbacku za pomocą inteligentnych wskaźników.