**How to Implement a Custom Allocator for std::vector in C++20?**

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

  • allocate uses the global operator new and records the allocation.
  • deallocate frees memory and updates the counter.
  • alloc_count is 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

  1. Not providing rebind – Containers instantiate the allocator for internal types (e.g., std::allocator_traits needs rebind).
  2. Wrong deallocation count – Ensure deallocate receives the same size n that was passed to allocate.
  3. Exception safety – If allocate throws, container must not leak memory.
  4. 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!

发表评论