In modern C++ (since C++11), the Standard Library containers, including std::vector, allow the programmer to provide a custom allocator. A custom allocator can control memory allocation strategies, logging, pooling, or even memory mapped files. This article walks through the design, implementation, and usage of a simple custom allocator that counts allocations and deallocations while delegating the actual memory management to the global operator new and operator delete.
1. Why Use a Custom Allocator?
- Performance tuning – A pool allocator can reduce fragmentation and improve cache locality.
- Memory profiling – Count allocations to detect leaks or excessive allocations.
- Special environments – Use shared memory, memory‑mapped files, or GPU memory.
- Debugging – Verify that containers use the intended allocator.
2. Allocator Requirements
A C++ allocator must satisfy the Allocator requirements of the C++ Standard. The minimal interface consists of:
| Function | Purpose |
|---|---|
allocate(std::size_t n) |
Allocate storage for n objects of type T. |
deallocate(T* p, std::size_t n) |
Deallocate previously allocated storage. |
| `rebind | |
| ::other` | Allows the allocator to allocate memory for a different type. |
max_size() |
Maximum number of objects that can be allocated. |
pointer, const_pointer, size_type, difference_type, etc. |
Type aliases. |
In C++20, the requirements are simplified, but rebind is still needed for container compatibility.
3. Basic Implementation Skeleton
#include <cstddef>
#include <memory>
#include <atomic>
#include <iostream>
template <typename T>
class CountingAllocator {
public:
using value_type = T;
using size_type = std::size_t;
using difference_type = std::ptrdiff_t;
using pointer = T*;
using const_pointer = const T*;
using reference = T&;
using const_reference = const T&;
using propagate_on_container_move_assignment = std::true_type;
template <class U>
struct rebind { using other = CountingAllocator <U>; };
constexpr CountingAllocator() noexcept = default;
template <class U>
constexpr CountingAllocator(const CountingAllocator <U>&) noexcept {}
pointer allocate(size_type n, const void* = nullptr) {
pointer p = static_cast <pointer>(::operator new(n * sizeof(T)));
alloc_count.fetch_add(1, std::memory_order_relaxed);
std::cout << "Alloc " << n << " objects (" << sizeof(T) << " bytes each), " << "ptr=" << static_cast<void*>(p) << '\n';
return p;
}
void deallocate(pointer p, size_type n) noexcept {
std::cout << "Dealloc " << n << " objects, ptr=" << static_cast<void*>(p) << '\n';
::operator delete(p);
alloc_count.fetch_sub(1, std::memory_order_relaxed);
}
static std::atomic<std::size_t> alloc_count;
};
template <typename T>
std::atomic<std::size_t> CountingAllocator<T>::alloc_count{0};
Explanation
allocateuses the globaloperator newand records the allocation.deallocatefrees memory and updates the counter.alloc_countis a static atomic counter shared across all instantiations of `CountingAllocator ` (per type).
4. Using the Allocator with std::vector
#include <vector>
#include <iostream>
int main() {
std::vector<int, CountingAllocator<int>> vec;
vec.reserve(10); // Triggers allocation
for (int i = 0; i < 10; ++i) vec.push_back(i);
std::cout << "Active allocations: " << CountingAllocator<int>::alloc_count.load() << '\n';
vec.clear(); // Does not deallocate capacity
vec.shrink_to_fit(); // Forces deallocation
std::cout << "Active allocations after shrink: " << CountingAllocator<int>::alloc_count.load() << '\n';
}
Output (example)
Alloc 10 objects (4 bytes each), ptr=0x55e1c9d0f260
Active allocations: 1
Dealloc 10 objects, ptr=0x55e1c9d0f260
Active allocations after shrink: 0
5. Extending the Allocator
| Feature | Implementation Idea |
|---|---|
| Pool allocator | Maintain a free list of blocks; on allocate, pop from list; on deallocate, push back. |
| Memory‑mapped file | Use mmap/CreateFileMapping to back allocations with a file. |
| Alignment control | Override allocate to use std::aligned_alloc or platform‑specific APIs. |
| Instrumentation | Record timestamps, thread IDs, or stack traces to diagnose leaks. |
| Thread safety | Use locks or lock‑free data structures for shared pools. |
6. Common Pitfalls
- Not providing
rebind– Containers instantiate the allocator for internal types (e.g.,std::allocator_traitsneedsrebind). - Wrong deallocation count – Ensure
deallocatereceives the same sizenthat was passed toallocate. - Exception safety – If
allocatethrows, container must not leak memory. - Alignment – Some containers (e.g., `std::vector `) may need special handling.
7. When to Use a Custom Allocator
- Profiling a library or engine where memory usage patterns matter.
- Embedded systems with constrained memory and deterministic allocation patterns.
- GPU or DSP programming where standard heap is unsuitable.
If your goal is simply to monitor allocations, the CountingAllocator shown above is often enough. For performance-critical applications, consider a fully featured pool allocator like boost::pool or implement your own lock-free allocator.
8. Summary
Custom allocators in C++ provide a powerful mechanism to tailor memory management to your application’s needs. By satisfying the allocator requirements and integrating with containers, you can add logging, pooling, or even alternative memory spaces without changing the rest of your codebase. The CountingAllocator example demonstrates the core concepts and shows how easily a container can be instrumented.
Happy allocating!