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 global operator new and operator delete.
  • std::pmr::null_memory_resource(): A resource that throws std::bad_alloc on 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++.