Type Erasure in C++ Polymorphic Memory Resources

Polymorphic memory resources (std::pmr) introduced in C++17 eliminate allocator-induced type bloat by shifting allocation strategy decisions from compile-time templates to runtime polymorphism. In traditional C++, custom allocators form part of a container's static type, making containers with identical elements but different allocators incompatible. Polymorphic memory resources use object-oriented type erasure via the abstract std::pmr::memory_resource base class, allowing containers to decouple their core data types from concrete allocation strategies without parameterizing the container itself over specific allocator implementations.

The Problem: Allocator Contagion

Standard library containers default to using std::allocator<T>, defined as:

template <class T, class Allocator = std::allocator<T>>
class vector;

Because the allocator is a template parameter, std::vector<int, CustomAllocatorA<int>> and std::vector<int, CustomAllocatorB<int>> are completely distinct types. Consequently:

  • Functions expecting one container type cannot accept another.
  • Containers cannot be reassigned or swapped without converting and copying elements.
  • Code bloat increases because the compiler instantiates separate container templates for every allocator variant.

How PMR Employs Type Erasure

PMR solves this problem by using standard object-oriented dynamic polymorphism to "erase" the concrete allocator type behind a uniform interface.

1. The Abstract Base Interface

At the core of the PMR model is std::pmr::memory_resource, an abstract base class that exposes pure virtual functions:

class memory_resource {
public:
    void* allocate(size_t bytes, size_t alignment = alignof(max_align_t));
    void deallocate(void* p, size_t bytes, size_t alignment = alignof(max_align_t));
    bool is_equal(const memory_resource& other) const noexcept;

protected:
    virtual void* do_allocate(size_t bytes, size_t alignment) = 0;
    virtual void do_deallocate(void* p, size_t bytes, size_t alignment) = 0;
    virtual bool do_is_equal(const memory_resource& other) const noexcept = 0;
};

Any concrete allocation scheme—such as a monotonic buffer, a thread-safe pool, or a custom stack allocator—inherits from std::pmr::memory_resource and overrides do_allocate and do_deallocate.

2. The Universal Adapter

To interface with standard containers that still require an allocator fulfilling the standard allocator requirements, the standard library provides std::pmr::polymorphic_allocator<T>.

Unlike custom allocators, std::pmr::polymorphic_allocator<T> does not implement its own memory management logic. Instead, it internally holds a pointer to a std::pmr::memory_resource:

template <class T>
class polymorphic_allocator {
    memory_resource* m_resource;
    // ...
};

When an allocation request is made, polymorphic_allocator<T> forwards the call to m_resource->allocate(...), dispatching the call dynamically through the virtual method table (vtable).

3. Unified Container Types

Because the memory resource is stored as a pointer to the base class rather than as a template parameter, the concrete allocation mechanics are completely erased from the container's signature.

The standard library aliases all standard containers in the std::pmr namespace using polymorphic_allocator:

namespace std::pmr {
    template <class T>
    using vector = std::vector<T, std::pmr::polymorphic_allocator<T>>;
}

Now, std::pmr::vector<int> has a fixed static type. It can draw memory from:

  • A stack-allocated buffer via std::pmr::monotonic_buffer_resource
  • A pooled allocator via std::pmr::unsynchronized_pool_resource
  • The default global heap via std::pmr::new_delete_resource()

Regardless of which resource is passed into the vector's constructor at runtime, the container remains a std::pmr::vector<int>, enabling unified interfaces across functions and translation units.

Trade-Offs

The primary cost of this type erasure mechanism is performance overhead from dynamic dispatch. Traditional template-based allocators allow compilers to inline memory allocation code directly at compile time. PMR trades inline optimization for structural flexibility by introducing a virtual function call and pointer indirection for each allocation and deallocation request. In exchange, binaries are smaller, compilation times are faster, and interfaces remain modular.