Polymorphic Allocators in C++17
This post starts a miniseries about an almost unknown feature in C++17: polymorphic allocators. I often promised that I would write about polymorphic allocators. Today, I fulfill my promise.
Since C++98, you can fine-tune memory allocation in general but also for user-defined types or containers of the standard library. For example, the containers of the sequential and associative containers of the STL have a defaulted allocator parameter. For example, here are the containers std::string
, std::vector
, and std::unordered_map
:
template<class CharT, class Traits = std::char_traits<CharT>, class Allocator = std::allocator<CharT>> class basic_string; using string = basic_string<char>; template<class T, class Allocator = std::allocator<T>> class vector; template<class Key, class T, class Hash = std::hash<Key>, class KeyEqual = std::equal_to<Key>, class Allocator = std::allocator<std::pair<const Key, T>>> class unordered_map;
The memory is allocated from the heap by default, but this default is not always appropriate. Often, you want to apply another strategy. Here are a few ideas.
- You want to use the stack instead of the heap for memory allocation.
- You want to use your memory allocation strategy and reduce the number of memory allocations.
- You want to allocate contiguous memory blocks to benefit from CPU caching.
- You want to allocate but don’t deallocate memory.
- You want to reuse a memory pool.
- You want to have a thread-safe memory allocation.
- You want to use different allocators for different types.
- You want to change the allocation strategy for a type if its size increases.
With C++98, you could implement and use your memory allocator, but this was pretty challenging and error-prone. I have already written two posts about memory allocators in C++: “Memory Management with std::allocator
” and a guest post from Jonathan Müller “Memory Pool Allocators“.
Thanks to polymorphic allocators in C++17, your job becomes way easier. Before I dive into the details, let me give you an introductory example.
// polymorphicAllocator.cpp #include <array> #include <cstddef> #include <memory_resource> #include <vector> int main() { std::array<std::byte, 200> buf1; // (1) std::pmr::monotonic_buffer_resource pool1{buf1.data(), buf1.size()}; std::pmr::vector<int> myVec1{&pool1}; // (3) for (int i = 0; i < 5; ++i) { myVec1.push_back(i); } char buf2[200] = {}; // (2) std::pmr::monotonic_buffer_resource pool2{std::data(buf2), std::size(buf2)}; std::pmr::vector<int> myVec2{&pool2}; for (int i = 0; i < 200; ++i) { myVec2.push_back(i); } }
First, I allocate memory on the stack. I can use a byte
array (line 1) or a char array (line 2) for that job. Furthermore, I initialize a std:pmr::motonotic_buffer
with the address and the size of the already stack-allocated memory. The final step is that the std::pmr::vector
takes this memory resource.
The namespace component pmr stands for a polymorphic memory resource. All components of the polymorphic allocators are in this namespace. The containers of the STL have their pmr pendants. This pmr pendant is an alias for a std::vector
using the special allocator. The following two lines are equivalent:
std::pmr::vector<int> myVec1{&pool1}; std::vector<int, std::pmr::polymorphic_allocator<int>> myVec1{&pool1};
When you study the example polymorphicAllocator.cpp
, you may wonder: How could a vector of 200 int
‘s myVec2 fit into a char
array of 200 elements? The answer is simple: std::pmr::new_delete_resource()
as the so-called upstream allocator kicks in as a
fallback. C++17 provides a few predefined memory resources.
Predefined Memory Resources
I will apply the listed predefined memory resources in upcoming posts. Therefore, here is a short overview. All predefined memory resources are derived from the interface class std::pmr::memory_resource
. The class provides three public member functions allocate
, deallocate
, and is_equal.
Correspondingly, std:pmr::memory_resource
has three private virtual member functions do_allocate
, do_deallocate
, and do_is_equal
. Typically, a user-defined memory resource overwrites these three virtual member functions, as do the predefined memory resources:
Modernes C++ Mentoring
Do you want to stay informed: Subscribe.
std::pmr::new_delete_resource
Returns a pointer to a memory resource, calling the global new
and delete
. It is the default memory resource if not otherwise specified.
std::pmr::null_memory_resource
Returns a pointer to a “null” memory resource. Using this memory resource for allocating causes a std::bad_alloc
exception. This memory resource ensures you do not arbitrarily allocate memory on the heap.
std::pmr::synchronized_pool_resource
A class that creates a memory resource with less fragmentation that is thread-safe. This class acts as a wrapper around the default resource.
std::pmr::unsynchronized_pool_resource
A class that creates a memory resource with less fragmentation that is not thread-safe. This class acts as a wrapper around the default resource.
std::pmr::monotonic_buffer_resource
A class to create memory resources in a big chunk of memory that is not thread-safe. You can optionally pass it as a buffer. This memory resource is pretty fast and can only grow. It destroys the objects but does not free the memory.
I used in the previous program polymorphicAllocator.cpp
a std::pmr::monotonic_buffer
with a byte
-array (line 1) and a char
-array (line 2) as a passed buffer. Additionally, the vector myVec2
allocated the remaining memory with the default allocator (upstream allocator) std::pmr::new_delete_resource
.
What’s Next?
Admittedly, his was a technical post. In my next post, I will apply the theory and implement a tracking memory resource. This user-defined memory resource will track all allocations and deallocations of the upstream allocator.
Thanks a lot to my Patreon Supporters: Matt Braun, Roman Postanciuc, Tobias Zindl, G Prvulovic, Reinhold Dröge, Abernitzke, Frank Grimm, Sakib, Broeserl, António Pina, Sergey Agafyin, Андрей Бурмистров, Jake, GS, Lawton Shoemake, Jozo Leko, John Breland, Venkat Nandam, Jose Francisco, Douglas Tinkham, Kuchlong Kuchlong, Robert Blanch, Truels Wissneth, Mario Luoni, Friedrich Huber, lennonli, Pramod Tikare Muralidhara, Peter Ware, Daniel Hufschläger, Alessandro Pezzato, Bob Perry, Satish Vangipuram, Andi Ireland, Richard Ohnemus, Michael Dunsky, Leo Goodstadt, John Wiederhirn, Yacob Cohen-Arazi, Florian Tischler, Robin Furness, Michael Young, Holger Detering, Bernd Mühlhaus, Stephen Kelley, Kyle Dean, Tusar Palauri, Juan Dent, George Liao, Daniel Ceperley, Jon T Hess, Stephen Totten, Wolfgang Fütterer, Matthias Grün, Phillip Diekmann, Ben Atakora, Ann Shatoff, Rob North, Bhavith C Achar, Marco Parri Empoli, Philipp Lenk, Charles-Jianye Chen, Keith Jeffery, Matt Godbolt, and Honey Sukesan.
Thanks, in particular, to Jon Hess, Lakshman, Christian Wittenhorst, Sherhy Pyton, Dendi Suhubdy, Sudhakar Belagurusamy, Richard Sargeant, Rusty Fleming, John Nebel, Mipko, Alicja Kaminska, Slavko Radman, and David Poole.
My special thanks to Embarcadero | |
My special thanks to PVS-Studio | |
My special thanks to Tipi.build | |
My special thanks to Take Up Code | |
My special thanks to SHAVEDYAKS |
Modernes C++ GmbH
Modernes C++ Mentoring (English)
Rainer Grimm
Yalovastraße 20
72108 Rottenburg
Mail: schulung@ModernesCpp.de
Mentoring: www.ModernesCpp.org
Modernes C++ Mentoring,