Yes, you can implement the observer pattern without dynamic allocation. This can be particularly important in embedded systems or performance-critical code. Let's explore several approaches.
Using a fixed array to store observers:
#include <array>
#include <iostream>
class Player {
public:
static constexpr size_t MaxObservers{8};
using DamageCallback = void (*)(int NewHealth);
int AddObserver(DamageCallback Cb) {
for (size_t i = 0; i < MaxObservers; ++i) {
if (!Observers[i]) {
Observers[i] = Cb;
return i;
}
}
return -1; // Array full
}
void RemoveObserver(int Idx) {
if (Idx >= 0 && Idx < MaxObservers) {
Observers[Idx] = nullptr;
}
}
void TakeDamage(int Damage) {
Health -= Damage;
for (auto& Obs : Observers) {
if (Obs) Obs(Health);
}
}
private:
std::array<
DamageCallback, MaxObservers> Observers{};
int Health{100};
};
Using compile-time registration with static storage:
#include <array>
template <typename T, size_t MaxObservers>
class StaticSubject {
public:
using Callback = void(*)(const T& Event);
static int RegisterObserver(Callback Cb) {
for (size_t i = 0; i < MaxObservers; ++i) {
if (!Observers[i]) {
Observers[i] = Cb;
return i;
}
}
return -1;
}
static void UnregisterObserver(int Idx) {
if (Idx >= 0 && Idx < MaxObservers) {
Observers[Idx] = nullptr;
}
}
protected:
static void NotifyObservers(const T& Event) {
for (auto& Obs : Observers) {
if (Obs) Obs(Event);
}
}
private:
static inline std::array<
Callback, MaxObservers> Observers{};
};
struct PlayerEvent {
int NewHealth;
};
class
Player : public StaticSubject<
PlayerEvent, 8> {
public:
void TakeDamage(int Damage) {
Health -= Damage;
NotifyObservers({Health});
}
private:
int Health{100};
};
For more flexibility without dynamic allocation:
#include <array>
#include <bitset>
template<typename T, size_t PoolSize>
class ObjectPool {
public:
T* Acquire() {
for (size_t i = 0; i < PoolSize; ++i) {
if (!InUse[i]) {
InUse[i] = true;
return &Objects[i];
}
}
return nullptr;
}
void Release(T* Ptr) {
if (Ptr) {
size_t Idx = Ptr - Objects.data();
if (Idx < PoolSize) {
InUse[Idx] = false;
}
}
}
private:
std::array<T, PoolSize> Objects;
std::bitset<PoolSize> InUse;
};
class Observer {
// Observer implementation
};
class ObserverSystem {
public:
Observer* CreateObserver() {
return Pool.Acquire();
}
void DestroyObserver(Observer* Obs) {
Pool.Release(Obs);
}
private:
static constexpr size_t MaxObservers{32};
ObjectPool<Observer, MaxObservers> Pool;
};
Key considerations for allocation-free observers:
The main tradeoff is flexibility vs. memory usage:
Choose based on your specific needs:
Answers to questions are automatically generated and may not have been reviewed.
An overview of the options we have for building flexible notification systems between game components