std::pmr::memory_resource
Abstract base class for C++17 polymorphic memory resources; decouples allocation strategy from container type via runtime polymorphism.
std::pmr::memory_resourcesince C++17An abstract base class in <memory_resource> that defines a uniform virtual interface for memory allocation, allowing the allocation strategy of standard containers to be changed at runtime without altering their type.
Overview
The classical C++ allocator model bakes the allocator into the container's type: a std::vector<int, MyAlloc> and a std::vector<int> are distinct types, making interoperability painful. C++17's Polymorphic Memory Resources (PMR) solve this by separating the type of a container from its allocation behavior through a pointer to a memory_resource.
std::pmr::memory_resource sits in the std::pmr namespace and lives in <memory_resource>. It uses the Non-Virtual Interface (NVI) pattern: three public non-virtual methods (allocate, deallocate, is_equal) delegate to three private pure-virtual methods (do_allocate, do_deallocate, do_is_equal). Derived classes override the do_* methods; callers only ever touch the public interface.
std::pmr::polymorphic_allocator<T> holds a memory_resource* and satisfies the standard Allocator requirements by forwarding to it. Every standard container has a std::pmr:: alias that uses polymorphic_allocator:
namespace std::pmr {
template <typename T>
using vector = std::vector<T, polymorphic_allocator<T>>; // C++17
}Because pmr::vector<int> and pmr::vector<int> with different backing resources share the same type, they can be passed to the same function overloads.
Syntax
#include <memory_resource>
namespace std::pmr {
class memory_resource {
public:
virtual ~memory_resource() = default;
void* allocate(std::size_t bytes,
std::size_t alignment = alignof(std::max_align_t));
void deallocate(void* p,
std::size_t bytes,
std::size_t alignment = alignof(std::max_align_t));
bool is_equal(const memory_resource& other) const noexcept;
protected:
virtual void* do_allocate(std::size_t bytes, std::size_t alignment) = 0;
virtual void do_deallocate(void* p, std::size_t bytes, std::size_t alignment) = 0;
virtual bool do_is_equal(const memory_resource& other) const noexcept = 0;
};
// Global resource accessors β all return non-null, non-owning pointers
memory_resource* new_delete_resource() noexcept; // delegates to ::operator new/delete
memory_resource* null_memory_resource() noexcept; // always throws std::bad_alloc
memory_resource* get_default_resource() noexcept; // process-wide default
memory_resource* set_default_resource(memory_resource* r) noexcept; // returns previous
} // namespace std::pmrallocate throws std::bad_alloc (or a derived exception) on failure; it never returns null. is_equal answers the question: "can memory allocated from *this be safely deallocated by other?" This is distinct from object identity.
Built-in Concrete Resources
C++17 ships three concrete memory_resource implementations:
| Type | Thread-safe | Notes |
|---|---|---|
monotonic_buffer_resource | No | Bump-pointer allocator; frees all memory at once on destruction |
unsynchronized_pool_resource | No | Pool allocator for heterogeneous block sizes |
synchronized_pool_resource | Yes | Same as above but mutex-protected |
Both pool resources accept an optional pool_options struct with max_blocks_per_chunk and largest_required_pool_block fields to tune chunk sizes.
Examples
Arena Allocation from the Stack
monotonic_buffer_resource is the workhorse of PMR. It never frees individual allocations β memory is reclaimed in bulk when the resource is destroyed. Combined with a stack buffer, it eliminates heap traffic entirely for short-lived containers.
#include <array>
#include <cstddef>
#include <memory_resource>
#include <string>
#include <vector>
void process_batch(const std::vector<std::string>& input) {
std::array<std::byte, 8192> stack_buf; // C++17: std::byte
// Pass null_memory_resource() as upstream so overflow throws
// rather than silently spilling to the heap.
std::pmr::monotonic_buffer_resource arena{
stack_buf.data(), stack_buf.size(),
std::pmr::null_memory_resource()
};
std::pmr::vector<std::pmr::string> scratch{&arena};
scratch.reserve(input.size());
for (const auto& s : input)
scratch.emplace_back(s, &arena); // string storage also in arena
// All allocations lived in stack_buf β zero heap traffic.
} // arena destructor resets the bump pointer; nothing to free individuallyChaining Resources
Resources can be chained. The upstream resource acts as a fallback or overflow sink:
#include <memory_resource>
#include <vector>
void demo() {
// First tier: a 512-byte monotonic arena backed by the heap
std::pmr::monotonic_buffer_resource fast{512, std::pmr::new_delete_resource()};
// Unsynchronized pool on top: handles varied allocation sizes efficiently
std::pmr::unsynchronized_pool_resource pool{&fast};
std::pmr::vector<int> v{&pool};
for (int i = 0; i < 200; ++i)
v.push_back(i);
}Implementing a Custom memory_resource
The interface is intentionally minimal. A logging or instrumented allocator is straightforward:
#include <cstddef>
#include <memory_resource>
class tracked_resource final : public std::pmr::memory_resource {
public:
explicit tracked_resource(
std::pmr::memory_resource* upstream = std::pmr::get_default_resource())
: upstream_{upstream} {}
std::size_t allocated_bytes() const noexcept { return allocated_bytes_; }
private:
void* do_allocate(std::size_t bytes, std::size_t alignment) override {
void* p = upstream_->allocate(bytes, alignment); // propagates bad_alloc
allocated_bytes_ += bytes;
return p;
}
void do_deallocate(void* p, std::size_t bytes, std::size_t alignment) override {
allocated_bytes_ -= bytes;
upstream_->deallocate(p, bytes, alignment);
}
bool do_is_equal(const std::pmr::memory_resource& other) const noexcept override {
return this == &other;
}
std::pmr::memory_resource* upstream_;
std::size_t allocated_bytes_ = 0;
};
// Usage
tracked_resource tracker;
std::pmr::vector<double> v{&tracker};
v.resize(1000);
// tracker.allocated_bytes() reflects live heap usage from this containerdo_is_equal returns identity equality because two distinct tracked_resource instances cannot safely exchange memory β each tracks a different counter and may hold a different upstream.
Best Practices
- Always pass the upstream explicitly when overflow behavior matters. A
monotonic_buffer_resourceconstructed without an upstream defaults toget_default_resource(), which silently falls back to the heap. Passingnull_memory_resource()turns overflow into an immediatebad_alloc, surfacing bugs. - Extend lifetime carefully.
polymorphic_allocatorholds a raw, non-owningmemory_resource*. The resource must outlive every container that uses it. Declare the resource before the container in the same scope, or hold it alongside the container in a struct. - Use
unsynchronized_pool_resourcefor heterogeneous allocation sizes in a single thread. Pool resources amortize the overhead of many small allocations into larger slab allocations from the upstream. For single-threaded code,unsynchronized_pool_resourceavoids mutex overhead. - Prefer
pmr::aliases at API boundaries. Acceptingpmr::vector<T>&instead ofvector<T, SomeAlloc>&makes functions allocator-agnostic without templates. - Implement
do_is_equalcorrectly. If your resource uses shared state (a shared pool, a shared arena), two instances referring to the same backing store should returntrue. Getting this wrong causes undefined behavior when the standard library callsdeallocatethrough one allocator for memory obtained through another.
Common Pitfalls
- The default upstream is not
new_delete_resource()formonotonic_buffer_resourceβ it is the result ofget_default_resource()at construction time, which may have been replaced by a priorset_default_resourcecall. Make the upstream explicit. set_default_resourceis not thread-safe. It must be called before any multi-threaded use of the default resource, typically once at program startup.- PMR types and non-PMR types are not the same type.
std::vector<int>andstd::pmr::vector<int>are distinct; you cannot bind a reference of one to the other. This is expected, not a bug, but it surprises engineers migrating code. monotonic_buffer_resourcedoes not call destructors on individual elements. It simply reclaims the raw memory. Elements with non-trivial destructors must still be destroyed explicitly (the container does this on its own destruction), but if you "wink out" the arena without destroying the container first, destructors will not run β a source of resource leaks for types that own handles.- Alignment must be honored in custom implementations.
do_allocatereceives the required alignment; returning misaligned memory is undefined behavior regardless of whether the caller checks it.
Writing a Custom memory_resource
Subclass std::pmr::memory_resource and override exactly three private pure-virtual methods. The public API (allocate, deallocate, is_equal) is non-virtual and handles precondition checking before dispatching to your overrides β you never call the do_* methods directly.
The three pure-virtual methods
do_allocate(bytes, alignment) must return a pointer to at least bytes bytes of storage aligned to a multiple of alignment. The alignment argument is always a power of two and at most alignof(std::max_align_t) for standard containers, but custom code may request extended alignment (over-aligned types). If you cannot satisfy the request, throw std::bad_alloc or a derived exception β never return null. A minimal alignment-aware implementation rounds the current bump pointer up to the nearest multiple of alignment before advancing it.
do_deallocate(p, bytes, alignment) receives the same bytes and alignment that were passed to the corresponding do_allocate call. The standard guarantees these match, so you can use them to locate the right pool slab or log the exact size. You must not throw. Monotonic resources implement this as a no-op; pool resources return the block to a free-list.
do_is_equal(other) answers whether *this and other share the same backing store, meaning memory allocated from one can be safely deallocated by the other. Return this == &other for resources with private state. Resources that wrap a shared upstream should cast other to their concrete type and compare the upstream pointer. Getting this wrong causes undefined behavior when the standard library copies allocators between containers.
#include <cstddef>
#include <memory_resource>
#include <new>
// A fixed-capacity bump-pointer resource that never calls upstream.
// Good for demonstrating the minimal contract.
class stack_resource final : public std::pmr::memory_resource {
public:
explicit stack_resource(void* buf, std::size_t cap) noexcept
: buf_{static_cast<std::byte*>(buf)}, cap_{cap} {}
private:
void* do_allocate(std::size_t bytes, std::size_t alignment) override {
// Align current offset upward
std::size_t space = cap_ - used_;
void* ptr = buf_ + used_;
if (!std::align(alignment, bytes, ptr, space))
throw std::bad_alloc{};
used_ = cap_ - space + bytes;
return ptr;
}
void do_deallocate(void*, std::size_t, std::size_t) noexcept override {
// Bump-pointer: individual frees are no-ops
}
bool do_is_equal(const std::pmr::memory_resource& o) const noexcept override {
return this == &o;
}
std::byte* buf_;
std::size_t cap_;
std::size_t used_ = 0;
};std::align (from <memory>) is the standard utility for advancing a pointer to the next aligned boundary; use it rather than hand-rolling bit masking to avoid UB on non-power-of-two alignments.
null_memory_resource and new_delete_resource
std::pmr::null_memory_resource() returns a singleton whose do_allocate always throws std::bad_alloc. Its primary use is as an intentionally failing upstream β wiring it as the overflow sink of a monotonic_buffer_resource turns any allocation that would spill to the heap into an immediate diagnostic failure. This is valuable in tests and in hard real-time code where heap allocation is prohibited.
std::pmr::new_delete_resource() returns a singleton that delegates directly to ::operator new and ::operator delete. It is the implicit upstream for monotonic_buffer_resource when you construct the resource with only a size, and it is what get_default_resource() returns unless overridden. Both singletons have static storage duration and are always valid to dereference.
monotonic_buffer_resource Deep Dive
monotonic_buffer_resource is a bump-pointer (also called linear or arena) allocator. Each call to do_allocate advances an internal pointer by the requested amount (plus any alignment padding) and returns the old position. do_deallocate is a no-op. All memory is released at once when the resource is destroyed or when release() is called explicitly.
Construction patterns
#include <memory_resource>
// 1. Size only β preallocates an internal buffer of at least `n` bytes
// from get_default_resource(); upstream is also get_default_resource().
std::pmr::monotonic_buffer_resource mbr1{4096};
// 2. External buffer β zero heap allocation until the buffer is exhausted.
// Upstream is get_default_resource() unless you supply one.
alignas(std::max_align_t) std::byte local[4096];
std::pmr::monotonic_buffer_resource mbr2{local, sizeof(local)};
// 3. External buffer + explicit upstream β overflow throws instead of
// silently falling back to the heap.
std::pmr::monotonic_buffer_resource mbr3{
local, sizeof(local), std::pmr::null_memory_resource()};Growth factor and upstream fallback
When the current buffer is full, monotonic_buffer_resource requests a new, larger chunk from the upstream resource. The standard does not mandate a specific growth factor, but implementations typically double the previous chunk size. This means the total wasted capacity is bounded by the size of the last chunk β the same amortized O(1) cost as std::vector growth. Because the resource never frees individual chunks, the upstream sees a series of ever-larger allocate calls with a single burst of deallocate calls at destruction time.
Resetting vs destroying
release() frees all chunks obtained from the upstream (except the initial externally-supplied buffer, which it merely resets the pointer for) and reinitializes the resource to its original state. The resource is usable again immediately after release(). This makes monotonic_buffer_resource ideal for per-frame or per-request allocators:
void render_loop(Renderer& r) {
std::array<std::byte, 64 * 1024> frame_buf;
std::pmr::monotonic_buffer_resource frame_arena{
frame_buf.data(), frame_buf.size(), std::pmr::null_memory_resource()};
while (r.running()) {
// Build per-frame data structures in the arena
std::pmr::vector<DrawCall> calls{&frame_arena};
r.collect_draw_calls(calls);
r.submit(calls);
frame_arena.release(); // reset pointer; no heap traffic
}
}Destroying the monotonic_buffer_resource also calls release() internally, so you do not need to call it manually at the end of a scope β it is only useful mid-lifetime for reuse.
Pool Resources: unsynchronized_pool_resource and synchronized_pool_resource
Pool resources maintain a collection of free-lists, one per power-of-two block size, up to a configurable maximum. This makes them efficient for workloads with many allocations of varied but repeated sizes β the typical allocation pattern of a node-based container like std::pmr::list or std::pmr::map.
pool_options
Both pool resources accept an optional std::pmr::pool_options struct:
struct pool_options {
std::size_t max_blocks_per_chunk = 0; // 0 == implementation-defined
std::size_t largest_required_pool_block = 0; // 0 == implementation-defined
};max_blocks_per_chunk: limits how many blocks are allocated in a single slab from the upstream. Smaller values reduce peak memory; larger values reduce upstream call frequency.largest_required_pool_block: the largest allocation size that must be served from a pool. Requests larger than this go directly to the upstream resource (without pooling). Setting it to, say,1024means that 2 KB allocations bypass the pool entirely.
Implementations are permitted to round these values to implementation-defined limits. Query the actual values in effect with the options() member function after construction.
#include <memory_resource>
std::pmr::pool_options opts;
opts.max_blocks_per_chunk = 32;
opts.largest_required_pool_block = 512;
std::pmr::unsynchronized_pool_resource pool{opts, std::pmr::new_delete_resource()};Geometric growth and oversized allocations
Each pool's slab list grows geometrically: the first slab holds a small number of blocks; subsequent slabs are larger, up to max_blocks_per_chunk. This keeps the number of upstream calls logarithmic in the total number of allocations. Requests that exceed largest_required_pool_block are forwarded directly to the upstream as single large allocations and are freed immediately on deallocate rather than returned to a free-list.
Thread safety
unsynchronized_pool_resource performs no synchronization and must be used from a single thread at a time. synchronized_pool_resource wraps each operation with a mutex, allowing concurrent allocation from multiple threads. Prefer the unsynchronized variant in worker threads that own their own resource instance.
Chaining Resources: The Zero-Heap Allocation Pattern
A monotonic_buffer_resource can serve as the upstream for a pool resource. The pool handles fragmentation (reusing freed blocks of the same size), while the monotonic arena provides a bounded, heap-free backing store for the slabs the pool allocates. When all sizes are known in advance, this chain can eliminate every heap allocation:
#include <array>
#include <cstddef>
#include <memory_resource>
#include <map>
#include <string>
void process_config(const RawConfig& raw) {
// Tier 1: fixed stack arena β no heap involved
std::array<std::byte, 16 * 1024> arena_buf;
std::pmr::monotonic_buffer_resource arena{
arena_buf.data(), arena_buf.size(),
std::pmr::null_memory_resource() // blow up on overflow
};
// Tier 2: pool resource draws its slabs from the arena
std::pmr::unsynchronized_pool_resource pool{&arena};
// All node allocations for the map come from the pool,
// which in turn draws from arena_buf on the stack.
std::pmr::map<std::pmr::string, int> cfg{&pool};
for (auto& [k, v] : raw.entries())
cfg.emplace(std::pmr::string{k, &pool}, v);
// At end of scope: pool frees slabs back to arena (no-op);
// arena destructor resets the bump pointer.
// Zero heap allocations if arena_buf was large enough.
}This pattern is common in game engines, embedded firmware, and high-frequency trading systems where heap allocation latency is unacceptable within a hot path.
polymorphic_allocator and the Uses-Allocator Protocol
std::pmr::polymorphic_allocator<T> is the bridge between a memory_resource* and the standard Allocator requirements. It stores a non-owning pointer to a memory_resource and forwards allocate/deallocate calls to it.
Allocator propagation to elements
When a pmr::vector<T> constructs elements of a type T that is itself allocator-aware (i.e., std::uses_allocator<T, std::pmr::polymorphic_allocator<std::byte>> is true), the vector automatically passes its own allocator to each element's constructor via the uses-allocator construction protocol. This means the element's internal allocations also come from the same memory_resource as the container β no opt-in boilerplate required.
The standard library PMR types β pmr::string, pmr::deque, pmr::list, pmr::map, pmr::unordered_map, etc. β all satisfy std::uses_allocator. A pmr::vector<pmr::string> will therefore route both the vector's own buffer allocation and each string's character storage through the same resource:
#include <memory_resource>
#include <string>
#include <vector>
void example() {
std::array<std::byte, 32 * 1024> buf;
std::pmr::monotonic_buffer_resource arena{buf.data(), buf.size()};
// Both the vector's element array AND each string's character
// data land in `arena` β because pmr::string satisfies uses_allocator.
std::pmr::vector<std::pmr::string> words{&arena};
words.emplace_back("hello");
words.emplace_back("world");
}std::pmr::string
std::pmr::string is a type alias for std::basic_string<char, std::char_traits<char>, std::pmr::polymorphic_allocator<char>>. It is a distinct type from std::string, so it cannot be bound to a std::string&. Conversions between the two require an explicit construction or assignment. At API boundaries, prefer std::string_view as the parameter type to accept both without copies.
construct and destroy
polymorphic_allocator provides construct and destroy member functions (in addition to the allocate/deallocate pair). construct uses std::make_obj_using_allocator under the hood to correctly inject the allocator into types that support it, following the uses-allocator construction rules. You rarely need to call these directly β the standard containers call them automatically.
set_default_resource and get_default_resource
The process-wide default resource starts as new_delete_resource(). Any memory_resource constructed without an explicit upstream falls back to get_default_resource() at the time of construction (not at the time of the first allocation).
set_default_resource(r) atomically installs r as the new default and returns the previous pointer. Passing nullptr restores new_delete_resource(). The function is not thread-safe with respect to concurrent calls β it must be called before any multi-threaded use of resources that depend on the default.
Unit test override pattern
A common testing technique replaces the default resource with one that tracks or forbids allocation, runs the system under test, then restores the original:
#include <gtest/gtest.h>
#include <memory_resource>
class null_resource_guard {
public:
null_resource_guard()
: prev_{std::pmr::set_default_resource(
std::pmr::null_memory_resource())} {}
~null_resource_guard() {
std::pmr::set_default_resource(prev_);
}
private:
std::pmr::memory_resource* prev_;
};
TEST(MySystem, NoHeapAllocationsDuringHotPath) {
null_resource_guard guard; // any implicit heap allocation now throws
MySystem sys;
// If hot_path() tries to allocate from the default resource, bad_alloc fires.
EXPECT_NO_THROW(sys.hot_path());
}Alternatively, install a tracked_resource (see the example above) instead of null_memory_resource() to count rather than forbid allocations β useful for regression-testing allocation budgets.
Lifetime requirements
The installed default resource must outlive every subsequent call to get_default_resource() and every resource that captured it as its upstream at construction time. Installing a resource with automatic storage duration and then returning from that scope is undefined behavior. Use static or dynamic storage for resources intended as long-lived defaults.
See Also
std::pmr::monotonic_buffer_resourceβ bump-pointer resource for arena-style allocationstd::pmr::polymorphic_allocatorβ standard Allocator adapter overmemory_resource*std::pmr::pool_optionsβ tuning struct for pool resourcesstd::allocator_traitsβ the compile-time allocator interface thatpolymorphic_allocatorsatisfies