C++
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Object-Oriented C++
-
Section 4: Memory Management
-
Section 5: Templates and Generic Programming
-
Section 6: The Standard Template Library
-
Section 7: Modern C++ Features
-
Section 8: Error Handling
-
Section 9: Multithreading
-
Section 10: Operator Overloading and Type Conversion
-
Section 11: Advanced Topics
-
Section 12: Tooling and Build Systems
-
Section 13: Design Patterns in C++
-
Section 14: Interfacing with C and Systems Programming
-
Section 15: Networking and IPC Basics
-
Section 16: Graphics and Game Programming Basics
-
Section 17: The Boost Libraries
-
Section 18: Data Structures and Algorithms in C++
-
Section 19: Practical Projects
-
Section 20: More Concurrency Patterns
-
Section 21: More OOP and Design Practice
-
Section 22: File I/O and Streams
-
Section 23: More Standard Library
-
Section 24: Practice Exercises
-
Section 25: Interview and Algorithm Practice
-
Section 26: Compiler and Language Internals
-
Section 27: GUI and Application Frameworks Overview
-
Section 28: Testing and Quality Practices
-
Section 29: Numerics Library
-
Section 30: Concepts Library (C++20)
-
Section 31: Ranges Library (C++20) In Depth
-
Section 32: More Utility Library
173: Building a Simple Memory Pool Allocator
Imagine you're running a hotel. If every single time a guest wanted to stay for a night, you had to call a construction crew to build a brand-new room from scratch, and then bulldoze that room the moment the guest checked out, you'd be out of business in a week. It's an absurd waste of time and resources. Instead, you build a block of 50 identical rooms once. When a guest arrives, you just hand them a key to an empty room. When they leave, you put the key back on the hook. The rooms are already there; you're just managing who is using them.
In C++, new and delete (or malloc and free) are like that construction crew. They talk to the operating system, search the heap for a big enough hole, and handle bookkeeping. If you're doing this thousands of times per second—say, for bullets in a shooter or particles in a physics sim—your performance will tank due to allocation overhead and heap fragmentation. A memory pool is our "hotel." We grab one giant chunk of memory upfront and carve it into equal-sized "rooms."
Why we stop asking the OS for every byte
The real killer isn't just the speed of the call; it's fragmentation. Over time, the heap becomes a Swiss cheese of small used and unused holes. Eventually, you might have plenty of total free memory, but not a single contiguous block large enough for a new object. By using a pool, we isolate a specific area of memory for objects of one size, ensuring we never fragment the rest of the system with tiny, short-lived allocations.
Turning a raw buffer into a linked list of slots
The cleverest part of a simple pool is the "free list." We don't want to maintain a separate std::vector or array to track which slots are empty—that would defeat the purpose of saving memory. Instead, we use the empty slots themselves to store the address of the next empty slot. It's a linked list that lives inside the unused memory.
template <typename T>
class MemoryPool {
union Slot {
T element;
Slot* next;
};
Slot* freeList = nullptr;
Slot* poolBuffer = nullptr;
public:
MemoryPool(size_t count) {
// Allocate one giant block of memory
poolBuffer = static_cast<Slot*>(::operator new(count * sizeof(Slot)));
// Chain all slots together into the initial free list
for (size_t i = 0; i < count - 1; ++i) {
poolBuffer[i].next = &poolBuffer[i + 1];
}
poolBuffer[count - 1].next = nullptr;
freeList = poolBuffer;
}
~MemoryPool() {
::operator delete(poolBuffer);
}
T* allocate() {
if (!freeList) return nullptr; // Pool exhausted
Slot* slot = freeList;
freeList = freeList->next;
return &slot->element;
}
void deallocate(T* ptr) {
// Cast the pointer back to a Slot and put it back at the front of the list
Slot* slot = reinterpret_cast<Slot*>(ptr);
slot->next = freeList;
freeList = slot;
}
};
I used a union here because a slot is either holding a live object (T element) or it's acting as a pointer to the next free spot (Slot* next). It can't be both at the same time, and this keeps the size of each slot exactly max(sizeof(T), sizeof(Slot*)).
Handling object lifetimes
Notice that my allocate method returns a raw pointer to memory, but it doesn't actually call the constructor of T. This is a common trap. The pool manages memory, not objects. If your class has a constructor that needs to run, you'll need to use "placement new."
When you allocate, you'd do: T* obj = new (pool.allocate()) T(args);. And when you're done, you must manually call the destructor obj->~T(); before calling pool.deallocate(obj);. It feels a bit manual, but that's the price you pay for this level of control. You're essentially telling the compiler, "Don't worry about finding memory; I've already provided the address, just initialize the object right here."
📋 Practical Task
Exercise: Optimizing a High-Frequency Particle Emitter
You are building a particle system for a game where 5,000 Particle objects are created and destroyed every few seconds. Using std::vector<Particle> with push_back and erase is causing noticeable frame stutters due to heap reallocations.
Your Task:
- Create a
Particlestruct that contains afloat x, y, vx, vy;and afloat lifetime;. - Implement a
ParticlePoolusing the memory pool pattern discussed in this lesson, pre-allocating space for 10,000 particles. - Write a loop that simulates a "burst" of particles:
- Allocate 1,000 particles using the pool and placement new.
- Store these pointers in a temporary list.
- Iterate through them, call their destructors, and return them to the pool via
deallocate.
- Verify that the pool can be reused by performing this burst operation 10 times without the application's memory usage growing linearly.
There are no comments for now.