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
Comprehensive course covering advanced concepts, and how to use them on large-scale projects.
View Course