Паттерн в одном абзаце

Наблюдатель позволяет объекту (субъекту) уведомлять список зависимых (наблюдателей), когда его состояние меняется — не зная, кто они и что делают. Субъект хранит список обратных вызовов; наблюдатели регистрируют себя; субъект вызывает каждый callback при изменении. Это всё.


Версия 1 — std::function, список с выделением на куче

Простейшая современная реализация на C++. Наследование на стороне наблюдателя не требуется — подходит любой вызываемый объект.

 1#include <functional>
 2#include <vector>
 3#include <algorithm>
 4
 5template <typename... Args>
 6class Event {
 7public:
 8    using Handler = std::function<void(Args...)>;
 9    using Token   = size_t;
10
11    Token subscribe(Handler h) {
12        Token id = nextId_++;
13        handlers_.push_back({id, std::move(h)});
14        return id;
15    }
16
17    void unsubscribe(Token id) {
18        handlers_.erase(
19            std::remove_if(handlers_.begin(), handlers_.end(),
20                           [id](const Entry& e) { return e.id == id; }),
21            handlers_.end());
22    }
23
24    void emit(Args... args) const {
25        for (const auto& e : handlers_)
26            e.handler(args...);
27    }
28
29private:
30    struct Entry { Token id; Handler handler; };
31    std::vector<Entry> handlers_;
32    Token nextId_ = 0;
33};

Использование:

 1Event<float> onTemperature;
 2
 3// Подписка — лямбда, метод класса или любой вызываемый объект
 4auto token = onTemperature.subscribe([](float t) {
 5    if (t > 80.0f) triggerFan();
 6});
 7
 8// В другом модуле:
 9auto token2 = onTemperature.subscribe([&display](float t) {
10    display.showTemp(t);
11});
12
13// При чтении датчика:
14onTemperature.emit(readSensor());
15
16// Отписаться, когда больше не нужно
17onTemperature.unsubscribe(token);

Ограничения

  • std::vector выделяет на куче — проблематично на встраиваемых платформах
  • std::function имеет собственное выделение в малом буфере для больших замыканий
  • Не потокобезопасно — вызов emit во время subscribe/unsubscribe — UB

Версия 2 — фиксированный размер, без кучи (безопасна для встраиваемых)

Заменяет std::vector массивом фиксированного размера. Ёмкость — параметр шаблона.

 1#include <array>
 2#include <functional>
 3#include <cstddef>
 4
 5template <typename Signature, size_t MaxSubscribers = 8>
 6class StaticEvent;
 7
 8template <typename... Args, size_t MaxSubscribers>
 9class StaticEvent<void(Args...), MaxSubscribers> {
10public:
11    using Handler = void(*)(Args...);  // обычный указатель на функцию — без выделения
12
13    bool subscribe(Handler h) {
14        for (auto& slot : slots_) {
15            if (!slot) { slot = h; return true; }
16        }
17        return false;  // полный
18    }
19
20    void unsubscribe(Handler h) {
21        for (auto& slot : slots_)
22            if (slot == h) { slot = nullptr; return; }
23    }
24
25    void emit(Args... args) const {
26        for (const auto& slot : slots_)
27            if (slot) slot(args...);
28    }
29
30private:
31    std::array<Handler, MaxSubscribers> slots_{};
32};

Использование:

 1// Глобальная или статическая — без кучи
 2static StaticEvent<void(float), 4> onTemperature;
 3
 4void displayTemp(float t) { display.showTemp(t); }
 5void checkAlarm (float t) { if (t > 80.0f) alarm.trigger(); }
 6
 7onTemperature.subscribe(displayTemp);
 8onTemperature.subscribe(checkAlarm);
 9
10onTemperature.emit(readSensor());
11
12onTemperature.unsubscribe(displayTemp);

Ограничения:

  • Только обычные указатели на функции — нет лямбд с захватом, нет методов класса
  • Фиксированная ёмкость, задаётся на этапе компиляции
  • subscribe молча возвращает false при заполнении — добавьте assert в отладочных сборках

Версия 3 — методы класса через указатель на член

Для подписки метода класса без std::function:

 1template <typename T, typename... Args>
 2class MemberEvent {
 3public:
 4    using MemberFn = void (T::*)(Args...);
 5
 6    void subscribe(T* obj, MemberFn fn) {
 7        observer_ = obj;
 8        method_   = fn;
 9    }
10
11    void emit(Args... args) const {
12        if (observer_) (observer_->*method_)(args...);
13    }
14
15private:
16    T*       observer_ = nullptr;
17    MemberFn method_   = nullptr;
18};
19
20// Использование:
21class Display {
22public:
23    void onTemp(float t) { /* показать t */ }
24};
25
26MemberEvent<Display, float> onTemperature;
27Display display;
28onTemperature.subscribe(&display, &Display::onTemp);
29onTemperature.emit(25.3f);

Это вариант с единственным подписчиком — при необходимости легко расширить до массива.


Выбор варианта

Сценарий Использовать
Десктоп / Qt-приложение, куча доступна Event<Args...> с std::function
Встраиваемый, без кучи, обычные callback’и StaticEvent с указателями на функции
Один наблюдатель, метод класса MemberEvent с указателем на член
Уже используется Qt signals + slots

Замечание о потокобезопасности

Ни одна из этих реализаций не является потокобезопасной. Для многопоточного использования защищайте subscribe/unsubscribe/emit мьютексом — или, если emit всегда вызывается из одного потока (например, главного цикла), ограничьте subscribe/unsubscribe тем же потоком.

На FreeRTOS: подписывайтесь во время инициализации до запуска планировщика, затем emit только из одной задачи — безопасно без какой-либо блокировки.


Полный заголовочный файл

 1// event.h — вставьте в любой проект
 2#pragma once
 3#include <array>
 4#include <cstddef>
 5
 6template <typename Signature, size_t N = 8>
 7class Event;
 8
 9template <size_t N, typename... Args>
10class Event<void(Args...), N> {
11public:
12    using Fn = void(*)(Args...);
13
14    bool on(Fn f) {
15        for (auto& s : s_) if (!s) { s = f; return true; }
16        return false;
17    }
18    void off(Fn f) {
19        for (auto& s : s_) if (s == f) { s = nullptr; return; }
20    }
21    void emit(Args... a) const {
22        for (auto s : s_) if (s) s(a...);
23    }
24private:
25    std::array<Fn, N> s_{};
26};

16 строк. Никаких зависимостей. Вставьте и используйте.