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 intsStatic 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 globalnewanddelete.
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.