Observer Pattern Without Dynamic Allocation
Is it possible to implement the observer pattern without dynamic memory allocation?
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.
Fixed-Size Array Approach
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};
};
Static Observer Registration
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};
};
Object Pool Approach
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:
- Determine maximum number of observers at compile time
- Handle the "out of space" scenario gracefully
- Consider using a pool for more complex observer types
- Be mindful of stack size when using fixed arrays
- Consider using static storage for global observers
The main tradeoff is flexibility vs. memory usage:
- Fixed arrays are simple but waste memory if underused
- Static registration is efficient but less flexible
- Object pools provide a middle ground
Choose based on your specific needs:
- Embedded systems: Fixed arrays or static registration
- Game development: Object pools for better memory usage
- Performance-critical code: Any approach that avoids allocation
Delegates and the Observer Pattern
An overview of the options we have for building flexible notification systems between game components