Allocators and std::pmr
A container gets its memory from an allocator. The default one calls new and delete, which is right for most code and wrong for a program that allocates many small objects or wants to free everything at once.
C++17 added std::pmr, a family of containers and strings that take a memory_resource instead of an allocator type. The resource decides where the bytes come from: a buffer on the stack, a pool, or the default new. This lesson uses the three that cover most needs.
A resource is an object like any other, so it can be a member of a class and be shared by every container that class owns. That is how a subsystem gets its own memory policy without changing its container types.
The pool also has a maximum block size, above which it forwards to its upstream resource, so a large allocation does not defeat it.
A buffer on the stack
A std::pmr::monotonic_buffer_resource hands out pieces of a buffer it was given and never frees them individually. A std::pmr::vector<int> constructed with &pool takes its memory from that buffer.
The program reserves a kilobyte on the stack, creates the resource over it and pushes five ints. The size and the last value are printed, and no call to new happened for the vector's storage.
The resource frees the whole buffer at once when it is destroyed, which makes it the tool for a phase of work that allocates a lot and then ends. The objects must not outlive the resource, which the last section covers.
#include <iostream>
#include <memory_resource>
#include <vector>
int main()
{
char buffer[1024];
std::pmr::monotonic_buffer_resource pool{buffer, sizeof(buffer)};
std::pmr::vector<int> values{&pool};
for (int i = 0; i < 5; ++i) {
values.push_back(i);
}
std::cout << values.size() << ' ' << values[4] << '\n';
}
g++ -std=c++23 -Wall -Wextra pmr.cpp -o pmr
./pmr
5 4
A pool resource
A std::pmr::unsynchronized_pool_resource keeps blocks of several sizes and reuses them. It is the choice for many small allocations and deallocations, where the monotonic resource would only grow.
The program uses a pool for a vector and a string, and prints their contents. The pool serves both and returns their blocks when they are destroyed, which the monotonic resource cannot do.
The unsynchronised version is faster and must not be used from several threads at once. The synchronised one, std::pmr::synchronized_pool_resource, is the safe default in concurrent code.
The two resources answer different questions. A monotonic buffer is for a burst of allocations that all end together; a pool is for a long-running program that allocates and frees the same sizes again and again.
#include <iostream>
#include <memory_resource>
#include <string>
#include <vector>
int main()
{
std::pmr::unsynchronized_pool_resource pool;
std::pmr::vector<int> values{&pool};
values.push_back(3);
values.push_back(1);
values.push_back(2);
std::pmr::string text{"pooled", &pool};
std::cout << values.size() << ' ' << text << '\n';
}
g++ -std=c++23 -Wall -Wextra pool.cpp -o pool
./pool
3 pooled
A resource that counts
A memory_resource is an interface with three functions: allocate, deallocate and is_equal. Writing one that forwards to new and counts the calls shows how many allocations a container makes.
The program reserves room for eight ints and pushes eight. With the reserve the count is one; without it the vector would grow several times, which is the measurement of lesson 019.
The same technique counts the bytes a phase of work uses, or routes the allocations of one subsystem to a pool while the rest of the program keeps new.
The reserve is what makes the count one: without it the vector would ask for a new buffer at sizes 1, 2, 4 and 8, and each reallocation of a pmr container returns the old block to the resource.
#include <cstddef>
#include <iostream>
#include <memory_resource>
#include <vector>
class counting_resource : public std::pmr::memory_resource
{
public:
int allocations() const
{
return count_;
}
private:
void* do_allocate(std::size_t bytes, std::size_t alignment) override
{
++count_;
return std::pmr::new_delete_resource()->allocate(bytes, alignment);
}
void do_deallocate(void* pointer, std::size_t bytes, std::size_t alignment) override
{
std::pmr::new_delete_resource()->deallocate(pointer, bytes, alignment);
}
bool do_is_equal(const std::pmr::memory_resource& other) const noexcept override
{
return this == &other;
}
int count_ = 0;
};
int main()
{
counting_resource resource;
std::pmr::vector<int> values{&resource};
values.reserve(8);
for (int i = 0; i < 8; ++i) {
values.push_back(i);
}
std::cout << values.size() << ' ' << resource.allocations() << '\n';
}
g++ -std=c++23 -Wall -Wextra count.cpp -o count_alloc
./count_alloc
8 1
The resource outlives the objects
A container built on a resource stores a pointer to it. The resource must be destroyed after the container, or the container will call deallocate on a resource that no longer exists.
The program declares the pool first and the vector second, so the vector is destroyed first at the end of main. The reverse order would compile and would be undefined behaviour.
The rule is the one of lesson 042 and every other owner: the resource is a dependency, and the objects that use it must die first. A member declaration order in a class does the same job for a class.
#include <iostream>
#include <memory_resource>
#include <vector>
int main()
{
std::pmr::unsynchronized_pool_resource pool;
std::pmr::vector<int> values{&pool};
values.push_back(42);
std::cout << values.size() << ' ' << values[0] << '\n';
}
g++ -std=c++23 -Wall -Wextra lifetime.cpp -o lifetime_alloc
./lifetime_alloc
1 42
Exercises
- Put a small class with a destructor into a pmr::vector and check that the destructors run.
- Change the counting resource to count bytes instead of allocations and print the total.
- Use a pmr::string built from a long literal with the monotonic resource and print its size.
Questions
A new set of questions in a new order every time. Choose one answer for each, then check.
-
What does a monotonic_buffer_resource do with the memory it hands out?
Answer
It never frees individual blocks and releases everything when destroyed
The monotonic resource only moves a pointer forward and frees the whole buffer at once.
-
What does
std::pmr::vector<int> values{&pool};take from the pool?Answer
The storage for the elements
The container asks the resource for the buffer it needs; the elements live in that buffer.
-
What does this program print?
char b[1024]; std::pmr::monotonic_buffer_resource p{b, sizeof b}; std::pmr::vector<int> v{&p}; v.push_back(7); std::cout << v[0];Answer
7
The vector stores 7 in the buffer and the element is read back normally.
-
Which resource reuses freed blocks of several sizes?
Answer
unsynchronized_pool_resource
The pool keeps blocks for reuse, which the monotonic resource by design does not.
-
Why is the unsynchronized_pool_resource faster than the synchronized one?
Answer
It does not take a lock, so it must not be used from several threads
The unsynchronised version drops the locking, which is safe only when one thread uses it.
-
What does this program print?
std::pmr::unsynchronized_pool_resource pool; std::pmr::vector<int> v{&pool}; v.push_back(1); v.push_back(2); std::cout << v.size();Answer
2
The vector holds two elements; the pool only decides where the buffer lives.
-
What does
new_delete_resource()return?Answer
A resource that allocates with new and delete
It is the default resource, the one that forwards to the global operator new and delete.
-
What does this program print?
std::pmr::unsynchronized_pool_resource pool; std::pmr::vector<int> v{&pool}; v.push_back(1); v.push_back(2); std::cout << v.size();Answer
2
The vector holds two elements; the pool only decides where the buffer lives.
-
What does this program print?
#include <cstddef> #include <iostream> #include <memory_resource> #include <vector> struct counting : std::pmr::memory_resource { int count = 0; void* do_allocate(std::size_t bytes, std::size_t align) override { ++count; return std::pmr::new_delete_resource()->allocate(bytes, align); } void do_deallocate(void* p, std::size_t bytes, std::size_t align) override { std::pmr::new_delete_resource()->deallocate(p, bytes, align); } bool do_is_equal(const std::pmr::memory_resource& other) const noexcept override { return this == &other; } }; int main() { counting resource; std::pmr::vector<int> values{&resource}; values.reserve(8); for (int i = 0; i < 8; ++i) values.push_back(i); std::cout << resource.count; }Answer
1
The reserve allocates the buffer once, and the eight pushes fit inside it.
-
What is the type of the elements of a
std::pmr::vector<int>?Answer
intwith a different allocatorThe element type is unchanged; pmr only changes the allocator, as an alias template does.
The code on this page was built with g++ 15.2.0 and run on Alpine Linux 3.24.3 (x86_64) in a Docker container.
Article author: Arthur Isaev