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::vectorallocates on the heap — problematic on embedded targetsstd::functionhas its own small-buffer allocation for large captures- Not thread-safe —
emitwhilesubscribe/unsubscribeis 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
subscribereturnsfalsesilently when full — add anassertin 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.