General-purpose new handles arbitrary sizes and lifetimes. Arenas trade flexibility for predictable bump allocation: advance an offset through a pre-allocated buffer in O(1) time, then free everything at once. Lesson 7.5 introduced std::pmr; this exercise implements the idea manually so you see the pointer arithmetic.
Keep a char* buffer, capacity, and offset. Each allocation aligns the current offset, returns buffer + offset, then advances offset by the requested size. reset() sets offset = 0, no per-object free list.
Given current address A and required alignment align, padding is (align - (A % align)) % align. Lesson 3.5 explained why alignment matters for SIMD and cache lines; the arena must honor alignof(T) for placement new.
The arena returns raw bytes; placement new constructs the object in that storage. There is no matching delete, call destructors manually if types are non-trivial before reset(), or restrict this exercise to trivially destructible Particle objects as documented.
void* mem = arena.allocate(sizeof(Particle), alignof(Particle));
Particle* p = new (mem) Particle{1.0f, 2.0f, 3.0f};
// p->~Particle(); // required before reset() for non-trivial types
No individual deallocate, no thread safety, no fallback when the buffer is exhausted except throwing. Production arenas often chain blocks or fall back to an upstream resource, std::pmr::monotonic_buffer_resource does exactly that.