C++ Memory Management Project
Write your own allocator: a pool that hands out memory and takes it back.
What you will build
Stop trusting malloc for a while: implement a memory pool that carves a big block into allocations, tracks free chunks, and reuses them without fragmentation chaos. You will understand — in your bones — what new and delete actually do.
What you will learn
- Free-list allocation strategies
- Alignment requirements
- Placement new and object lifetime
- Fragmentation: internal vs external
- RAII wrappers over raw memory
Feature checklist
- Arena allocator: bump-pointer with reset
- Free-list allocator: allocate/free any order
- Correct alignment for any type
- Allocation statistics (used, peak, fragmentation)
- A Pool<T> that constructs/destroys objects in place
- Benchmark vs new/delete
Starter code
#include <cstddef>
#include <cstdlib>
#include <new>
class Arena {
char* base_; char* cur_; size_t cap_;
public:
explicit Arena(size_t bytes)
: base_((char*)std::malloc(bytes)), cur_(base_), cap_(bytes) {}
~Arena() { std::free(base_); }
Arena(const Arena&) = delete;
void* alloc(size_t size, size_t align = alignof(std::max_align_t)) {
// round current offset up to the alignment boundary
size_t used = cur_ - base_;
size_t pad = (align - (used % align)) % align;
if (used + pad + size > cap_) return nullptr; // out of memory
void* p = cur_ + pad;
cur_ += pad + size;
return p;
}
template <typename T, typename... Args>
T* make(Args&&... args) {
void* mem = alloc(sizeof(T), alignof(T));
if (!mem) return nullptr;
return new (mem) T(std::forward<Args>(args)...); // placement new
}
void reset() { cur_ = base_; } // free everything at once
};Full walkthrough
Step 1: Start with WHY: allocation is a policy, not a detail
new/delete ask a general-purpose allocator to find space for any size, any lifetime — flexible, and comparatively slow, with fragmentation. Games and embedded code instead carve ONE big block upfront and hand out pieces under a strict policy. This project implements two policies so you can feel the trade-offs in numbers, not adjectives.
Step 2: Build the Arena: a bump pointer over one buffer
class Arena { char* buffer; size_t used, capacity; } — alloc(bytes) checks used + bytes <= capacity, returns buffer + used, and bumps used by bytes. That is the entire allocator: allocation is an addition. reset() sets used = 0 and the whole arena is free again in O(1) — no per-object free exists, which is the point and the limitation in one sentence.
void* Arena::alloc(size_t bytes, size_t align) {
uintptr_t cur = (uintptr_t)(buffer + used);
uintptr_t aligned = (cur + align - 1) & ~(align - 1);
size_t newUsed = (aligned - (uintptr_t)buffer) + bytes;
if (newUsed > capacity) return nullptr;
used = newUsed;
return (void*)aligned;
}Step 3: Get alignment right, or nothing else matters
Every type has an alignment requirement (alignof(T)); an address for a double must be a multiple of 8. alloc() rounds the current offset UP to the requested alignment before carving — the bit trick (x + a - 1) & ~(a - 1). Skipping alignment appears to work on x86 until it does not, which makes it the most dangerous line in the project to omit.
Step 4: Add make<T>(): construct objects in the arena
alloc() returns raw memory; make<T>(args...) uses placement new — new (ptr) T(args...) — to actually construct an object there, and records a destructor call for reset-time if your design supports it (many arenas simply require trivially-destructible or POD-ish types and say so). Raw memory vs constructed object is THE distinction C++ forces you to understand.
Step 5: Build the free-list allocator for the other lifetime shape
Where the arena demands 'everything dies together', a free list allows alloc/free in any order: a linked list of free blocks threaded THROUGH the free memory itself; alloc takes the first block that fits (splitting it if much larger), free returns the block (ideally coalescing neighbours). More general, more bookkeeping, fragmentation included at no extra charge.
Step 6: Add a Pool<T> for same-type objects
A pool pre-carves the buffer into fixed-size slots of sizeof(T) with a free-slot list: allocation is popping a slot, construction is placement new, destruction calls ~T() and pushes the slot back. Uniform size removes splitting and coalescing entirely — pools are why entity systems and particle engines do not fragment.
Step 7: Measure: statistics and a benchmark
Track used, peak, and (for the free list) a fragmentation estimate; print them. Then benchmark: a million small allocations through the arena vs new/delete, timed with std::chrono like the primes project. The arena should win by a wide margin — if it does not, your benchmark is measuring something else (usually construction, not allocation), and finding that out is the real exercise.
Common mistakes
- Returning unaligned offsets from the arena, corrupting or crashing on types with alignment requirements — round up, always.
- Calling delete on arena memory (or new on pool slots) — each allocator's memory is owned by its backing buffer; the lifetimes are manual and documented.
- Benchmarking allocation plus construction against pure allocation and concluding the allocator 'does nothing'.
Compile and run
g++ -std=c++17 -o memory memory.cppSample run
$ ./memory
Arena demo — one 1 MiB buffer, bump-pointer allocation
alloc(256 B) -> offset 0
alloc(100 B, align 16) -> offset 256
make<Point>(3, 4) constructed in-place at offset 368
Stats: used 376 B · peak 376 B · capacity 1048576 B
reset() -> used 0 B (the whole arena freed in O(1))
Free-list demo — alloc/free in any order
alloc A(512) B(512) C(512) · free B · alloc D(256) reuses B's block
Fragmentation report: 1 free block of 256 B inside the used region
Pool<Point> demo — 3 live objects, destroyed and slots recycled
Benchmark: 1,000,000 small allocations
arena: 4 ms
new/delete: 61 ms
Same machine, same work — the policy is the difference.Try it in your browser
No browser demo is included for this project. Build the C++ version below — that is the real program.