Kelick Tech Hub

Build, learn & keep up — tech, Linux & sports news included.

C++ Memory Management Project

Advanced•1–2 weeks•custom allocator, RAII, memory pools

Write your own allocator: a pool that hands out memory and takes it back.

Share on X

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

Feature checklist

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

Compile and run

g++ -std=c++17 -o memory memory.cpp

Sample 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.

Related Advanced projects

← All 60 projects · Classic site (Learn & Sandbox)

Download Fake GPS