How C++ PMR Standardizes Allocator Interfaces
The Polymorphic Memory Resources (PMR) framework, introduced in C++17, standardizes custom memory management by decoupling a container's allocation strategy from its static type. Historically, C++ allocators were embedded directly into container type signatures, causing severe interoperability and code bloat issues. PMR resolves this by using an abstract base class and runtime polymorphism, allowing developers to swap memory allocation mechanisms—such as pool, monotonic, or arena allocators—without altering container types or rewriting API interfaces.
The Problem with Traditional C++ Allocators
Prior to C++17, standard containers accepted an allocator as a template parameter:
std::vector<T, Allocator>Because the allocator is part of the type signature, two containers
with the same element type but different allocators are fundamentally
incompatible. A function designed to accept a
std::vector<int, std::allocator<int>>&
cannot accept a
std::vector<int, CustomStackAllocator<int>>&.
This design caused several architectural problems:
- Interface Fragmentation: Library authors had to
either force users to use
std::allocator, make their APIs heavily templated to accommodate arbitrary allocator types, or write wrapper layers. - Code Bloat: Templating functions on allocator types generates redundant compiled code for each allocator configuration.
- Complex Customization: Injecting specialized memory handling (e.g., stack buffers or arena allocators) into existing codebases often required invasive refactoring.
The PMR Architecture: Moving from Compile-Time to Runtime
The PMR framework shifts memory allocation from compile-time
templates to runtime polymorphism through three core components defined
in <memory_resource>:
1.
std::pmr::memory_resource
At the foundation of PMR is the abstract base class
std::pmr::memory_resource. It defines pure virtual
functions for low-level memory operations:
do_allocate(std::size_t bytes, std::size_t alignment)do_deallocate(void* p, std::size_t bytes, std::size_t alignment)do_is_equal(const std::pmr::memory_resource& other) const noexcept
Public non-virtual member functions (allocate,
deallocate, and is_equal) call these private
virtual methods, following the Non-Virtual Interface (NVI) idiom. Any
custom allocation logic simply inherits from
memory_resource and overrides these methods.
2.
std::pmr::polymorphic_allocator<T>
To bridge the gap between standard containers and
std::pmr::memory_resource, C++ provides
std::pmr::polymorphic_allocator<T>. This class
fulfills the standard allocator requirements but internally stores a
non-owning pointer to a std::pmr::memory_resource.
When a container requests memory,
std::pmr::polymorphic_allocator<T> delegates the
request to the underlying memory_resource pointer via
virtual function dispatch.
3. Standard Container Type Aliases
The std::pmr namespace provides pre-defined type aliases
for all standard containers using
polymorphic_allocator:
namespace std::pmr {
template <typename T>
using vector = std::vector<T, std::pmr::polymorphic_allocator<T>>;
template <typename T>
using string = std::basic_string<char, std::char_traits<char>, std::pmr::polymorphic_allocator<char>>;
}Because std::pmr::vector<int> has a fixed type, it
can be passed across boundaries, stored in interfaces, and reused
regardless of which memory resource backs it at runtime.
Standardized Built-in Memory Resources
PMR standardizes several common allocation strategies out of the box:
std::pmr::monotonic_buffer_resource: Allocates sequentially from a provided buffer or growing heap chunks and never deallocates until the entire resource is destroyed. Ideal for short-lived, high-throughput tasks like frame-based operations in game loops.std::pmr::unsynchronized_pool_resource: Organizes memory into a collection of pools containing fixed-size chunks to minimize fragmentation and optimize allocations of varying sizes in a single thread.std::pmr::synchronized_pool_resource: A thread-safe variant of the pool resource.std::pmr::new_delete_resource(): Returns a global pointer to a resource that delegates to the standard globaloperator newandoperator delete.std::pmr::null_memory_resource(): A resource that throwsstd::bad_allocon any allocation attempt, useful for guaranteeing that an operation will not exceed a pre-allocated buffer.
Practical Impact on API Design
PMR standardizes how C++ applications handle memory by eliminating
the need to choose between performance and clean API design. APIs can
accept types like const std::pmr::vector<int>& in
function signatures, while callers maintain full control over whether
that vector uses stack memory, a shared arena, or standard heap
memory:
#include <array>
#include <vector>
#include <memory_resource>
void process_data(const std::pmr::vector<int>& data);
void run() {
std::array<std::byte, 1024> stack_buffer;
std::pmr::monotonic_buffer_resource mem_pool(stack_buffer.data(), stack_buffer.size());
// Allocates directly from stack_buffer without heap overhead
std::pmr::vector<int> numbers(&mem_pool);
numbers.push_back(10);
numbers.push_back(20);
// Passed seamlessly without template parameters
process_data(numbers);
}By establishing a common polymorphic base and unifying container signatures, PMR provides a consistent, standardized vocabulary for custom memory management across modern C++.