Memory Pools
By the end of this lesson you'll understand why per-object new / delete is slow in a hot loop, and you'll be able to build a fixed-block pool allocator and a bump-pointer arena by hand, recycle objects with an object pool, and reach for std::pmr when you want the speed without writing the allocator yourself.
Learn Python, JavaScript, Java and more with free interactive lessons, real projects and AI-powered help. Beginner-friendly.
Part of the free C++ course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.
Calling new for every object is like driving to the store for one ingredient each time you cook — generic, flexible, and slow because each trip has fixed overhead. A pool is a tray of identical lunchboxes by the door: grab one when you need it, drop it back when you're done, and an empty box is instantly reusable — no searching, no oddly-shaped gaps. An arena is a whiteboard: you keep writing left to right (bumping a pointer), and when the meeting ends you wipe the whole board in one stroke instead of erasing each note. General new / delete is the all-purpose store; pools and arenas are purpose-built and far faster for the right pattern .
A pool or arena removes nearly all of this for a known pattern: allocation becomes "pop one node" or "bump a pointer", and the objects sit packed together for cache-friendly iteration. That's why game engines, network servers, and compilers lean on them in their hottest loops.
1. The Fixed-Block Pool Allocator
A pool carves one big block into equal-sized slots and hands them out one at a time. The clever bit is the free list : while a slot is unused, you reuse its own bytes to store a pointer to the next free slot. Allocation pops the head of that list and deallocation pushes a slot back onto it — both are O(1) with zero extra memory. Read this worked example carefully; every non-obvious line is commented with what it does.
A raw byte pool is fine for learning, but in real code you want a typed object pool that constructs a real object in the slot. That needs placement new — new (ptr) T(args) runs a constructor on memory you already own, without allocating anything. Because placement new never pairs with delete , you must call the destructor yourself with ptr->~T() before recycling the slot.
2. The Arena (Bump) Allocator
An arena (also called a bump allocator) is even simpler than a pool. It keeps one cursor — an offset — and every allocation just hands back the current position then moves the cursor forward. You never free individual objects; instead you reset() the whole arena, which rewinds the cursor to 0 and reclaims everything at once in O(1) . This is ideal for data with a clear phase : per-frame game state, per-request server scratch memory, or a compiler's AST nodes. Now you finish one.
The program below is almost complete — fill in the two blanks marked ___ using the // 👉 hints, then run it and check it against the expected output.
3. std::pmr — Standard Allocators Without the Boilerplate
Hand-writing allocators is great for understanding, but C++17 gives you a ready-made framework: std::pmr (polymorphic memory resources). A std::pmr::memory_resource is an object that knows how to hand out memory, and pmr containers like pmr::vector take their memory from it. Two come built in: monotonic_buffer_resource is an arena (bump, reset on destruction), and unsynchronized_pool_resource is a pool. You get custom-allocator speed without writing the allocator — and you can swap strategies without changing the container's type.
Pools and arenas hand out raw bytes , not constructed objects. Placement new bridges that gap: new (ptr) T(args) calls T 's constructor at the address ptr and allocates nothing. The price is symmetry — there is no "placement delete", so you must run the destructor yourself: ptr->~T(); . Forget that, and any work the destructor does (closing files, freeing inner buffers) silently leaks.
Alignment matters too: a double usually must sit on an 8-byte boundary. A union-of- T slot (as in the object pool) is automatically aligned for T ; a raw char buffer is not, so a real arena rounds each offset up to the right boundary before handing it out.
Key calls: new (ptr) T(args) (placement new), ptr->~T() (manual destructor), #include for std::pmr .
No blanks this time — just a brief and an outline. Build a fixed-block pool for a tiny Token type, hand out and recycle slots, and prove a freed slot gets reused by comparing pointers. This is the exact pattern a real engine uses for particles, entities, and packets.
Practice quiz
Why is per-object new/delete slow in a hot loop compared to a pool?
- It searches free lists, may split/merge blocks, and often takes a lock
- It always zero-initialises the memory it returns
- It compresses the heap on every call
- It runs the constructor twice for safety
Answer: It searches free lists, may split/merge blocks, and often takes a lock. A general allocator does bookkeeping (find/split/merge a block) and is usually thread-safe (a lock), while a pool just pops one node.
In a fixed-block pool's free list, where is the 'next free slot' pointer stored?
- In a separate side table indexed by slot number
- Inside the unused slot's own bytes
- In a global hash map keyed by address
- At the end of the buffer, after all slots
Answer: Inside the unused slot's own bytes. While a slot is free its bytes are reused to hold the next pointer, so the pool needs zero extra bookkeeping memory.
What are the time complexities of pool allocate and deallocate?
- O(n) allocate, O(1) deallocate
- O(log n) for both
- O(1) for both — pop the head, push it back
- O(n) for both
Answer: O(1) for both — pop the head, push it back. Allocation pops the head of the free list and deallocation pushes a slot back, both O(1).
What does placement new — new (ptr) T(args) — do?
- Allocates fresh memory and constructs T in it
- Runs T's constructor on memory you already own, allocating nothing
- Frees ptr and then reallocates T
- Only zeroes the bytes at ptr
Answer: Runs T's constructor on memory you already own, allocating nothing. Placement new constructs an object at an address you provide; it allocates no memory itself.
After constructing an object with placement new, how do you destroy it before recycling the slot?
- Call delete on the pointer
There is no placement delete, so you must run the destructor yourself with ptr->~T(); never call delete on placement-new memory.
How does an arena (bump) allocator free everything?
- It calls delete on each object in reverse order
- It walks a free list and frees each node
- It resets the offset cursor back to 0 in O(1)
- It cannot free — the OS reclaims it at exit
Answer: It resets the offset cursor back to 0 in O(1). An arena hands out memory by bumping an offset; reset() rewinds the offset to 0, reclaiming everything at once.
In the arena's allocate(), how is the cursor advanced after handing back the current position?
- offset = offset + bytes;
- offset = 0;
- offset = bytes - offset;
- offset = sizeof(buffer);
Answer: offset = offset + bytes;. Each allocation returns buffer + offset, then moves offset forward by the requested number of bytes.
Which std::pmr resource behaves like an arena (bump-allocate, reclaim on destruction)?
- std::pmr::synchronized_pool_resource
- std::pmr::monotonic_buffer_resource
- std::pmr::new_delete_resource
- std::pmr::null_memory_resource
Answer: std::pmr::monotonic_buffer_resource. monotonic_buffer_resource bump-allocates and never reuses freed memory, releasing it all when the resource dies.
Which header must you include to use std::pmr, and from which standard?
- <memory_resource>, C++17
- <memory>, C++11
- <pmr>, C++20
- <allocator>, C++14
Answer: <memory_resource>, C++17. std::pmr (polymorphic memory resources) lives in <memory_resource> and was added in C++17.
When is it appropriate to reach for a custom pool or arena allocator?
- Always — it is faster than new/delete in every program
- Only in a proven hot path after a profiler shows allocation is the bottleneck
- Whenever a class has a destructor
- Only when the program is single-threaded
Answer: Only in a proven hot path after a profiler shows allocation is the bottleneck. Default new/delete is fine for most code; measure first and use a custom allocator only where allocation is genuinely the bottleneck.