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.