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

Questions & Answers

Answers are generated by AI models and may not have been reviewed. Be mindful when running any code on your device.

Why Use Delegates?
Why do we need to use delegates instead of just calling functions directly?
Delegate Performance Impact
How does the performance of delegates compare to direct function calls?
Handling Multiple Event Types
Can I have an observer observe multiple different types of events from the same subject?
Pausing Observer Notifications
Is it possible to temporarily pause notifications to specific observers?
std::function vs Function Pointer Performance
What's the overhead of using std::function compared to raw function pointers?
Smart Pointers with Observers
Can I use smart pointers to automatically manage observer registration/unregistration?
Or Ask your Own Question
Get an immediate answer to your specific question using our AI assistant