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
120: Custom Allocators
If you're just starting to look into custom allocators, you probably have this thought: "Why would I bother writing my own? The system allocator is written by geniuses at Microsoft or GNU; surely they've already optimized it to the limit."
It's a reasonable assumption, but it's fundamentally wrong. The system allocator is a general-purpose tool. It has to handle a request for 16 bytes and a request for 16 megabytes with equal stability. To do that, it uses complex bookkeeping, locks for thread safety, and search algorithms to find a free hole in the heap that fits your request. When you're doing this 10,000 times a frame in a game loop or a high-frequency trading app, that "general purpose" logic becomes a massive bottleneck.
The Myth: System Allocators Are Always Fast Enough
Let's look at a concrete scenario. Imagine you're building a particle system. Every frame, you spawn 5,000 Particle objects and destroy them shortly after. If you use std::vector<Particle> with the default allocator, you're hitting the heap constantly. Even worse, if those particles are allocated individually via new, you're scattering them across your RAM.
// The "slow" way: hitting the general-purpose heap constantly
for(int i = 0; i < 5000; ++i) {
Particle* p = new Particle(); // System call, lock acquisition, bookkeeping
particles.push_back(p);
}
// ... later ...
for(auto p : particles) delete p; // More bookkeeping, fragmentation risk
In this case, the system allocator isn't slow because the code is bad; it's slow because it's doing too much work. It's trying to be fair to every other part of your program. When you write a custom allocator, you're telling the compiler: "I know exactly how I'm going to use this memory, so stop guessing and just give me a slab of it."
The Reality: Managing Memory vs. Managing Objects
One thing that trips people up when they first implement std::allocator is the distinction between allocation and construction. In a normal new call, these two things happen at once. With custom allocators, they are decoupled.
allocate(): This just grabs raw, uninitialized bytes. It doesn't know about constructors. It's like buying a plot of land.construct(): This is where the object is actually born (using placement new). This is like building the house on that land.
I've seen plenty of devs try to call the constructor inside allocate(). Don't do that. If you do, you'll break the way STL containers work, and you'll likely end up with memory leaks or double-construction bugs that are a nightmare to debug.
The Arena Strategy: Trading Flexibility for Raw Speed
The most effective "first" custom allocator I ever used was the Arena (or Linear) Allocator. The logic is dead simple: you allocate one giant block of memory upfront, and when someone asks for more, you just move a pointer forward. You don't "free" individual objects; you just wipe the whole arena clean at the end of the frame.
template <typename T>
class ArenaAllocator {
T* buffer;
size_t offset = 0;
size_t capacity;
public:
using value_type = T;
ArenaAllocator(size_t size) : capacity(size) {
buffer = static_cast<T*>(::operator new(size * sizeof(T)));
}
T* allocate(std::size_t n) {
if (offset + n > capacity) throw std::bad_alloc();
T* result = &buffer[offset];
offset += n;
return result;
}
void deallocate(T* p, std::size_t n) noexcept {
// We do NOTHING here. That's the secret.
// We reclaim everything at once by resetting the offset to 0.
}
void reset() { offset = 0; }
};
By ignoring deallocate, we've turned a complex O(log n) search for a free memory block into a simple O(1) pointer addition. It's incredibly fast, and because the memory is contiguous, your CPU cache will love you.
📋 Practical Task
Exercise: Implementing a Fixed-Size Pool Allocator
The Arena allocator we discussed is great for short-lived data, but it's useless if you need to delete objects in a random order. For that, we use a Pool Allocator.
Your task is to implement a PoolAllocator for a specific object type (e.g., struct Node { int data; Node* next; };). Instead of a simple pointer move, your allocator should maintain a "free list" of available slots.
Requirements:
- Pre-allocate a block of memory capable of holding 1,000
Nodeobjects. - Implement
allocate()so that it pops a pointer from the free list. - Implement
deallocate()so that it pushes the returned pointer back onto the free list for future reuse. - Verify that allocating 1,000 nodes, deleting 500 of them, and then allocating 500 more does not exceed the original pre-allocated memory block.
There are no comments for now.