Skip to content

Allocators — std::allocator, Custom Allocators, Pool Allocation

DodaTech Updated 2026-06-28 7 min read

In this tutorial, you will learn about Allocators. We cover key concepts, practical examples, and best practices to help you master this topic.

C++ allocators are memory resource adaptors that separate memory allocation from container logic, with std::allocator as the default and custom allocators enabling specialized strategies like pool allocation and arena-based allocation.

What You'll Learn

You will use std::allocator to allocate and construct objects, write a custom allocator for a memory pool, leverage std::scoped_allocator_adaptor for nested containers, understand polymorphic allocators from C++17 (std::pmr), and choose allocation strategies for different performance requirements.

Why It Matters

The default allocator (std::allocator) uses new and delete for every allocation. In performance-critical code, this can cause fragmentation and slowdowns. Custom allocators let you implement arena allocation, thread-local caching, and pool strategies that are essential in Game Development, real-time systems, and high-frequency trading.

Learning Path

graph LR
    A["25: Custom Deleters"] --> B["26: Allocators"]
    B --> C["27: Object Lifetimes"]
    C --> D["28: Memory Order"]
    style A fill:#4a90d9,stroke:#2c5f8a,color:#fff
    style B fill:#4a90d9,stroke:#2c5f8a,color:#fff
    style C fill:#4a90d9,stroke:#2c5f8a,color:#fff
    style D fill:#4a90d9,stroke:#2c5f8a,color:#fff

std::allocator Basics

#include <iostream>
#include <memory>

int main() {
    std::allocator<int> alloc;
    
    // Allocate raw memory for 5 ints
    int* p = alloc.allocate(5);
    
    // Construct objects in the allocated memory
    for (int i = 0; i < 5; ++i) {
        alloc.construct(p + i, i * 10);
    }
    
    // Use the objects
    for (int i = 0; i < 5; ++i) {
        std::cout << p[i] << " ";
    }
    std::cout << "\n";
    
    // Destroy objects
    for (int i = 0; i < 5; ++i) {
        alloc.destroy(p + i);
    }
    
    // Deallocate memory
    alloc.deallocate(p, 5);
}

Allocators separate memory allocation from object construction. allocate gets raw memory, construct calls the constructor, destroy calls the destructor, deallocate releases memory.

Writing a Custom Allocator

#include <iostream>
#include <memory>
#include <vector>
#include <cstdint>

template <typename T, size_t PoolSize = 1024>
class PoolAllocator {
private:
    char pool_[PoolSize];
    size_t offset_ = 0;
    
public:
    using value_type = T;
    
    PoolAllocator() noexcept : offset_(0) {}
    
    template <typename U>
    PoolAllocator(const PoolAllocator<U, PoolSize>&) noexcept {}
    
    T* allocate(size_t n) {
        size_t bytes = n * sizeof(T);
        if (offset_ + bytes > PoolSize) {
            throw std::bad_alloc();
        }
        T* result = reinterpret_cast<T*>(pool_ + offset_);
        offset_ += bytes;
        return result;
    }
    
    void deallocate(T*, size_t) noexcept {
        // Pool allocator does not support individual deallocation
    }
    
    template <typename U>
    bool operator==(const PoolAllocator<U, PoolSize>&) const {
        return true;
    }
    
    template <typename U>
    bool operator!=(const PoolAllocator<U, PoolSize>&) const {
        return false;
    }
};

int main() {
    std::vector<int, PoolAllocator<int>> vec;
    for (int i = 0; i < 100; ++i) {
        vec.push_back(i);
    }
    std::cout << "Vector size: " << vec.size() << "\n";
    for (int i = 0; i < 10; ++i) {
        std::cout << vec[i] << " ";
    }
    std::cout << "\n";
}

Arena Allocator

#include <iostream>
#include <memory>
#include <vector>

template <typename T>
class ArenaAllocator {
private:
    char* arena_;
    size_t arenaSize_;
    size_t offset_;
    
public:
    using value_type = T;
    
    ArenaAllocator(char* arena, size_t size) noexcept
        : arena_(arena), arenaSize_(size), offset_(0) {}
    
    template <typename U>
    ArenaAllocator(const ArenaAllocator<U>& other) noexcept
        : arena_(other.arena_), arenaSize_(other.arenaSize_), offset_(0) {}
    
    T* allocate(size_t n) {
        size_t bytes = n * sizeof(T);
        size_t aligned = (bytes + alignof(T) - 1) & ~(alignof(T) - 1);
        if (offset_ + aligned > arenaSize_) {
            throw std::bad_alloc();
        }
        T* result = reinterpret_cast<T*>(arena_ + offset_);
        offset_ += aligned;
        return result;
    }
    
    void deallocate(T*, size_t) noexcept {
        // Arena allocator: bulk deallocation at end
    }
    
    size_t used() const { return offset_; }
    void reset() { offset_ = 0; }
    
    template <typename U>
    bool operator==(const ArenaAllocator<U>& other) const {
        return arena_ == other.arena_;
    }
    
    template <typename U>
    bool operator!=(const ArenaAllocator<U>& other) const {
        return !(*this == other);
    }
    
    template <typename U>
    friend class ArenaAllocator;
    
private:
    char* arena_;
    size_t arenaSize_;
    size_t offset_;
};

int main() {
    char buffer[1024];
    ArenaAllocator<int> alloc(buffer, sizeof(buffer));
    std::vector<int, ArenaAllocator<int>> vec(alloc);
    
    for (int i = 0; i < 100; ++i) {
        vec.push_back(i);
    }
    
    std::cout << "Allocated " << alloc.used() << " bytes\n";
}

Polymorphic Allocators (C++17, <memory_resource>)

C++17 introduced std::pmr (polymorphic memory resource) allocators:

#include <iostream>
#include <memory_resource>
#include <vector>
#include <array>

int main() {
    // Monotonic buffer resource (arena)
    std::array<char, 2048> buffer;
    std::pmr::monotonic_buffer_resource pool(buffer.data(), buffer.size());
    
    std::pmr::vector<int> vec(&pool);
    for (int i = 0; i < 500; ++i) {
        vec.push_back(i);
    }
    
    std::cout << "Vector size: " << vec.size() << "\n";
    
    // Unsynchronized pool resource (thread-local)
    std::pmr::unsynchronized_pool_resource pool2;
    std::pmr::vector<double> vec2(&pool2);
    vec2.reserve(100);
    
    // Synchronized pool resource (thread-safe)
    std::pmr::synchronized_pool_resource pool3;
    std::pmr::vector<char> vec3(&pool3);
}

pmr allocators are designed to work together through the polymorphic std::pmr::memory_resource base class.

Allocator-Aware Containers

When using custom allocators with nested containers, the allocator must propagate to nested elements:

#include <iostream>
#include <memory>
#include <vector>
#include <map>
#include <string>

template <typename T>
using MyAlloc = std::allocator<T>;

// std::pmr::polymorphic_allocator handles nested allocation automatically
using StringVector = std::pmr::vector<std::pmr::string>;

int main() {
    std::pmr::monotonic_buffer_resource pool;
    StringVector vec(&pool);
    
    vec.push_back("Hello");
    vec.push_back("World");
    
    for (const auto& s : vec) {
        std::cout << s << " ";
    }
    std::cout << "\n";
}

Comparing Allocation Strategies

Strategy Allocation Deallocation Fragmentation Use Case
std::allocator new/delete Per-object High General purpose
Pool allocator O(1) bump Bulk reset None Many small objects
Arena allocator O(1) bump Bulk reset None Frame allocations
Stack allocator O(1) push O(1) pop None Nested lifetimes
pmr::monotonic_buffer_resource O(1) bump Bulk reset None General arena

Common Mistakes

Mistake 1: Custom Allocator That Cannot Rebind

template <typename T>
class MyAlloc {
    // Must provide rebind or the allocator traits
};

The rebind mechanism lets containers allocate internal nodes with a different type than value_type.

Mistake 2: Assuming Allocator is Used for All Container Allocations

Containers may use the default allocator for some internal structures. Test with a custom allocator to verify.

Mistake 3: Not Propagating on Copy/Move Assignment

Allocator-aware containers must handle allocator propagation on assignment (POCMA: propagate on container move assignment).

Mistake 4: Arena Overflows

char buffer[128];
ArenaAllocator<int> alloc(buffer, sizeof(buffer));
std::vector<int, ArenaAllocator<int>> vec(alloc);
for (int i = 0; i < 1000; ++i) vec.push_back(i);  // bad_alloc!

Mistake 5: Using std::vector<bool> with Custom Allocators

std::vector<bool> is a special case that may not use the allocator correctly for its bitset representation.

Mistake 6: Thread Safety of Custom Allocators

Pool and arena allocators are typically not thread-safe. Use thread-local instances or pmr::synchronized_pool_resource.

Practice Questions

  1. What is the purpose of allocators in C++?
  2. Write a minimal custom allocator that uses malloc and free.
  3. What does std::pmr::monotonic_buffer_resource do?
  4. Why might you use a pool allocator instead of the default allocator?
  5. How does rebinding work in allocators?

Challenge

Implement a StackAllocator that supports push/pop semantics (allocations go on top, deallocations must be in reverse order). Use it with std::vector and verify that it works correctly.

FAQ

Should I write custom allocators?

Only if you have measured a performance problem with the default allocator. Premature optimization with custom allocators adds complexity. Use pmr allocators from C++17 for most custom allocation needs.

What is `std::scoped_allocator_adaptor`?

It is an adaptor that propagates an allocator to nested container elements, ensuring that all containers in a hierarchy use the same allocation source.

Do custom allocators work with `std::string`?

Yes. std::string is allocator-aware: std::basic_string<char, std::char_traits<char>, MyAlloc<char>>.

What is the difference between `allocate` and `::operator new`?

allocate is an allocator member that returns raw memory. ::operator new is the global allocation function that std::allocator::allocate typically calls.

Are pmr allocators compatible with regular containers?

pmr containers (in std::pmr::) use std::pmr::polymorphic_allocator. Regular std::vector uses std::allocator. They are different types and cannot be mixed.

What is the 'rebind' mechanism?

Containers like std::list allocate nodes, not elements. Rebinding converts allocator<T> to allocator<Node<T>>. The standard requires custom allocators to support rebinding.

Mini Project

Build a frame allocator for game development:

#include <iostream>
#include <memory_resource>
#include <vector>

class FrameAllocator {
private:
    std::pmr::monotonic_buffer_resource resource_;
    std::pmr::polymorphic_allocator<int> alloc_;
    
public:
    FrameAllocator(size_t frameSize)
        : resource_(frameSize), alloc_(&resource_) {}
    
    std::pmr::vector<int> createVector() {
        return std::pmr::vector<int>(alloc_);
    }
    
    void clearFrame() {
        resource_.release();
    }
};

int main() {
    FrameAllocator frame(1024 * 1024);
    
    for (int frame = 0; frame < 10; ++frame) {
        auto vec = frame.createVector();
        for (int i = 0; i < 1000; ++i) {
            vec.push_back(i);
        }
        std::cout << "Frame " << frame << ": " << vec.size() << " elements\n";
        
        frame.clearFrame();  // All memory from this frame is reclaimed at once
    }
}

What's Next

Allocators control memory acquisition. The next lesson covers object lifetimes: storage duration (automatic, static, thread-local, dynamic), placement new intricacies, and alignment requirements.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro