Allocation is often the hidden cost in hot paths; custom memory resources can make code several times faster and more predictable.
Every standard container gets its memory through an allocator, the last template parameter: std::vector<T, Alloc>. The default, std::allocator<T>, calls operator new. A custom allocator only needs a value_type, allocate(n), deallocate(p, n), equality comparison and a converting constructor; std::allocator_traits fills in the rest:
template <class T> struct Arena {
using value_type = T;
T* allocate(std::size_t n);
void deallocate(T* p, std::size_t n) noexcept;
};The classic model has drawbacks. The allocator is part of the type, so vector<int, A> and vector<int, B> don't mix, and stateful allocators bring tricky propagation rules on copy, move and swap.
C++17's polymorphic memory resources (<memory_resource>, namespace std::pmr) solve this. A std::pmr::memory_resource is an abstract class with virtual allocate/deallocate. std::pmr::polymorphic_allocator forwards to one, and aliases like std::pmr::vector<T> and std::pmr::string use it, so containers backed by different resources share one type. The allocator also propagates to elements: a pmr::vector<pmr::string> places its strings' buffers in the same resource.
Standard resources:
monotonic_buffer_resource: bump-pointer arena. Allocation is a pointer increment,deallocateis a no-op, and everything is freed at once when the resource dies. Ideal for per-request or per-frame data, optionally starting from a stack buffer.unsynchronized_pool_resource/synchronized_pool_resource: pools of fixed-size blocks, without or with locking.new_delete_resource()andnull_memory_resource()(which always throws, handy as an upstream to catch overflow).
The usual caveat: the resource must outlive every container using it, and measure before and after to confirm allocation was actually the bottleneck.
Example
#include <array>
#include <cstddef>
#include <iostream>
#include <memory_resource>
#include <string>
#include <vector>
int main() {
std::array<std::byte, 4096> buffer; // arena on the stack
std::pmr::monotonic_buffer_resource arena{
buffer.data(), buffer.size(), std::pmr::null_memory_resource()}; // throw if exceeded
std::pmr::vector<std::pmr::string> names{&arena}; // vector AND strings use the arena
names.reserve(8);
names.emplace_back("a string long enough to need its own heap buffer");
names.emplace_back("short");
bool same = names[0].get_allocator().resource() == &arena; // allocator propagated
std::cout << names.size() << ' ' << same << '\n'; // 2 1
std::pmr::unsynchronized_pool_resource pool; // size-class pools
std::pmr::vector<int> nums{&pool};
nums.assign(1000, 7);
std::cout << nums.back() << '\n'; // 7
} // arena memory is released all at once; monotonic deallocate() is a no-opRemember
- Containers allocate through their allocator parameter
std::pmrputs the strategy in a runtime memory_resource, keeping one container type- Monotonic arenas make allocation a pointer bump and free everything at once
Watch out for
- Letting a memory resource be destroyed while a pmr container still uses it.