The pattern in one paragraph

Observer lets an object (subject) notify a list of dependents (observers) when its state changes — without knowing who they are or what they do. The subject holds a list of callbacks; observers register themselves; the subject calls each callback on change. That’s it.


Version 1 — std::function, heap-allocated list

The simplest modern C++ implementation. No inheritance required on the observer side — any callable works.

 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};

Usage:

 1Event<float> onTemperature;
 2
 3// Subscribe — lambda, member function, or anything callable
 4auto token = onTemperature.subscribe([](float t) {
 5    if (t > 80.0f) triggerFan();
 6});
 7
 8// In another module:
 9auto token2 = onTemperature.subscribe([&display](float t) {
10    display.showTemp(t);
11});
12
13// When sensor reads:
14onTemperature.emit(readSensor());
15
16// Unsubscribe when no longer needed
17onTemperature.unsubscribe(token);

Limitations

  • std::vector allocates on the heap — problematic on embedded targets
  • std::function has its own small-buffer allocation for large captures
  • Not thread-safe — emit while subscribe/unsubscribe is running is UB

Version 2 — fixed-size, no heap (embedded-safe)

Replaces std::vector with a fixed-size array. Capacity is a template parameter.

 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...);  // plain function pointer — no allocation
12
13    bool subscribe(Handler h) {
14        for (auto& slot : slots_) {
15            if (!slot) { slot = h; return true; }
16        }
17        return false;  // full
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};

Usage:

 1// Global or static — no heap
 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);

Constraints:

  • Only plain function pointers — no lambdas with captures, no member functions
  • Fixed capacity set at compile time
  • subscribe returns false silently when full — add an assert in debug builds

Version 3 — member functions via pointer-to-member

To subscribe a member function without 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// Usage:
21class Display {
22public:
23    void onTemp(float t) { /* show t */ }
24};
25
26MemberEvent<Display, float> onTemperature;
27Display display;
28onTemperature.subscribe(&display, &Display::onTemp);
29onTemperature.emit(25.3f);

This is a single-subscriber variant — simple to extend to an array if needed.


Choosing a variant

Scenario Use
Desktop / Qt app, heap available Event<Args...> with std::function
Embedded, no heap, plain callbacks StaticEvent with function pointers
Single observer, member function MemberEvent pointer-to-member
Already using Qt signals + slots

Thread safety note

None of these implementations are thread-safe. For multi-threaded use, protect subscribe/unsubscribe/emit with a mutex — or, if emit is always called from one thread (e.g. main loop), restrict subscribe/unsubscribe to the same thread.

On FreeRTOS: subscribe during initialisation before the scheduler starts, then emit from one task only — safe without any lock.


Full drop-in header

 1// event.h — drop into any project
 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 lines. No dependencies. Paste and use.