C++ PMR Containers vs Standard Containers

This article explores the technical differences between standard containers in the std namespace and their polymorphic counterparts in the std::pmr namespace introduced in C++17. It covers how memory allocation strategies differ between the two, the role of static versus dynamic dispatch, the impact on container type identity and binary interfaces, and the performance trade-offs associated with each approach.

Allocator Type Identity and ABI Stability

In the classic std namespace, containers are parameterized by their allocator type at compile time. For example, std::vector<T, Allocator> treats the allocator as an integral part of the vector's type signature. If two vectors use different allocators, they are distinct, incompatible types:

std::vector<int, std::allocator<int>> standardVec;
std::vector<int, MyCustomAllocator<int>> customVec;
// standardVec and customVec cannot be passed to the same non-templated function.

In contrast, std::pmr (Polymorphic Memory Resource) containers use a single, unified allocator type: std::pmr::polymorphic_allocator<T>. All std::pmr containers are type aliases for their standard counterparts using this specific allocator. For example, std::pmr::vector<T> is defined as std::vector<T, std::pmr::polymorphic_allocator<T>>.

Because the allocator type remains uniform, functions can accept std::pmr containers by reference without using templates, regardless of the underlying memory source:

void process(std::pmr::vector<int>& data); // Accepts any PMR vector of ints

Static vs. Dynamic Allocation Dispatch

Standard library allocators resolve allocation and deallocation operations at compile time. The compiler can inline calls to allocate() and deallocate(), which eliminates indirect call overhead. However, changing the allocation strategy requires recompilation and introduces new template instantiations.

std::pmr containers shift allocation dispatch to runtime through virtual function calls. The polymorphic_allocator wraps a pointer to an abstract base class, std::pmr::memory_resource, which defines pure virtual functions:

  • do_allocate(size_t bytes, size_t alignment)
  • do_deallocate(void* p, size_t bytes, size_t alignment)
  • do_is_equal(const memory_resource& other)

Every allocation or deallocation in a std::pmr container incurs the overhead of a virtual function dispatch via a vtable pointer.

Runtime Memory Strategy Flexibility

Standard containers generally default to allocating directly from the global heap using operator new and delete. Changing this behavior requires writing a fully compliant C++ allocator class and applying it as a template argument.

std::pmr allows switching memory strategies dynamically at runtime by passing different memory_resource instances to the container's constructor. The standard library provides several built-in resources:

  • std::pmr::monotonic_buffer_resource: Fast, arena-style allocation that releases memory only when the resource itself is destroyed, eliminating individual deallocation costs. It can be backed by a stack buffer to avoid heap access entirely.
  • std::pmr::unsynchronized_pool_resource: Organizes allocations into pools of fixed-size chunks to minimize fragmentation and reuse memory, optimized for single-threaded usage.
  • std::pmr::synchronized_pool_resource: A thread-safe variant of the pool resource.
  • std::pmr::new_delete_resource(): Forwards requests directly to global new and delete.

Performance Considerations

While std::pmr introduces virtual call overhead for each allocation, it often outperforms default std containers in scenarios with frequent allocations and deallocations. By combining std::pmr containers with a monotonic_buffer_resource or a stack-backed buffer, allocation reduces to a simple pointer bump, avoiding expensive system-level heap operations.

Conversely, for long-lived containers that allocate infrequently, standard containers are typically more optimal because they avoid the memory overhead of storing a pointer to the memory resource and the execution cost of dynamic dispatch.