Template Class PoolAllocator

Class Documentation

template<typename T, size_t C, typename L = SpinLock>
class PoolAllocator

Fixed-size object pool with O(1) allocation/deallocation using embedded freelist.

PoolAllocator pre-allocates a fixed buffer for C instances of type T and manages them using a freelist embedded directly in the free blocks. This makes allocation and deallocation O(1) pointer operations with LIFO reuse (last freed is first allocated), which tends to have good cache characteristics.

The freelist works by storing pointers to the next free block inside each free block itself. This is why the constraint sizeof(T) >= sizeof(void*) exists: each free block must be large enough to hold a pointer to the next free block. Types smaller than a pointer (like bool, char, uint16_t on 64-bit systems) cannot be pooled with this allocator.

Thread Safety: Controlled by template parameter L. By default uses SpinLock, which is ideal for very short-lived locks (microseconds) with rare contention. For longer critical sections or higher contention, substitute std::mutex: PoolAllocator<MyType, 1000, std::mutex>.

Example - Particle system with object pooling:

PoolAllocator<Particle, 1000> particle_pool;

void spawn_particle(Vector3 pos, Vector3 vel) {
    auto* particle = particle_pool.alloc(pos, vel);  // O(1) allocation
    active_particles.push_back(particle);
}

void update_particles(float dt) {
    for (auto it = active_particles.begin(); it != active_particles.end();) {
        (*it)->update(dt);
        if ((*it)->is_dead()) {
            particle_pool.free(*it);  // O(1) deallocation, returns to freelist
            it = active_particles.erase(it);
        } else {
            ++it;
        }
    }
}

Important Notes:

  • Allocation throws std::bad_alloc when pool is exhausted (all C slots used)

  • clear() rebuilds freelist WITHOUT calling destructors - only use when pool is empty or all objects are trivially destructible

  • Freelist reuse is LIFO order (last freed, first allocated)

  • Pool memory is never deallocated until PoolAllocator is destroyed

See also

BucketPoolAllocator for variable-sized allocations up to a bucket size

See also

SpinLock for the default lock implementation

Template Parameters:
  • T – The object type to pool. Must satisfy sizeof(T) >= sizeof(void*) because free blocks embed next-pointers in themselves. Common pooled types: game entities, particles, messages, temporary objects.

  • C – Pool capacity (maximum number of T instances). Pool size is C * sizeof(T). Set based on worst-case usage to avoid exhaustion.

  • L – Lock type for thread safety. Defaults to SpinLock. Use std::mutex for longer critical sections or high contention. Use a no-op lock type for single-threaded scenarios to eliminate locking overhead.

Public Functions

inline PoolAllocator() noexcept
template<typename ...Args>
inline T *alloc(Args&&... args)

Allocates and constructs a new object of type T

Template Parameters:

Args – Constructor argument types

Parameters:

args – Constructor arguments

Returns:

Pointer to the new object

inline void free(T *p)

Frees the specified allocated pointer

Parameters:

p – The pointer to free

inline void clear()

Rebuilds the freelist, making all pool slots available again.

WARNING: This does NOT call destructors on allocated objects. Calling clear() with active allocations causes RESOURCE LEAKS and UNDEFINED BEHAVIOR for types managing resources (heap memory, file handles, mutexes, etc.). This is a CORRECTNESS and SAFETY issue, not merely a performance concern.

Only use clear() when:

  • The pool is completely empty (all objects freed), OR

  • All allocated objects are trivially destructible (no resources to clean up)

For proper cleanup, call free() on ALL allocated objects before clear().

Public Static Attributes

static constexpr auto pool_size = C * sizeof(T)

Total size of the pool in bytes (C instances * sizeof(T)).