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