C++17: Polymorphic Allocators, Debug Resources and Custom Types
C++17: Polymorphic Allocators, Debug Resources and Custom Types
In this article, take a look at polymorphic allocators and see how to debug sources and custom types.
Join the DZone community and get the full member experience.Join For Free
In my previous article on polymorphic allocators, we discussed some basic ideas. For example, you've seen a
pmr::vector that holds
pmr::string using a monotonic resource. How about using a custom type in such a container? How to enable it? Let's see.
In the previous article there was similar code:
See the full example @Coliru
In this case, when you insert a new string into the vector, the new object will also use the memory resource that is specified on the vector.
And by "use" I mean the situation where the string object has to allocate some memory, which means long strings that don't fit into the Short String Optimisation buffer. If the object doesn't require any extra memory block to fetch, then it's just part of the contiguous memory blog of the parent vector.
pmr::string can use the vector's memory resource, it means that it is somehow "aware" of the allocator.
How about writing a custom type:
If I plug in this into the vector:
Then, the vector will use the provided memory resource but won't propagate it into
Product. That way if
Product has to allocate memory for
name it will use a default allocator.
We have to "enable" our type and make it aware of the allocators so that it can leverage the allocators from the parent container.
Before we start, I'd like to mention some good references if you'd like to try allocators on your own. This topic is not super popular, so finding tutorials or good descriptions is not that easy as I found.
- CppCon 2017: Pablo Halpern “Allocators: The Good Parts” - YouTube - in-depth explanations of allocators and the new PMR stuff. Even with a test implementation of some node-based container.
- CppCon 2015: Andrei Alexandrescu “std::allocator…” - YouTube - from the introduction you can learn than
std::allocatorwas meant to fix far/near issues and make it consistent, but right now we want much more from this system.
- c++ - What is the purpose of allocator_traits in C++0x? - Stack Overflow
- Jean Guegant’s Blog – Making a STL-compatible hash map from scratch - Part 3 - The wonderful world of iterators and allocators - this is a super detailed blog post on how to make more use of allocators, not to mention good anecdotes and jokes :)
- Thanks for the memory (allocator) - Sticky Bits - a valuable introduction to allocators, their story and how the new model of PMR fit in. You can also see how to write your tracking pmr allocator and how
- CppCon 2018: Arthur O’Dwyer “An Allocator is a Handle to a Heap” - a great talk from Arthur where he shares all the knowledge needed to understand allocators.
- C++17 - The Complete Guide by Nicolai Josuttis - inside the book, there’s a long chapter about PMR allocators.
Debug Memory Resource
To work efficiently with allocators, it would be handy to have a tool that allows us to track memory allocations from our containers.
See the resources that I listed on how to do it, but in a basic form, we have to do the following:
- Derive from
do_allocate()- the function that is used to allocate N bytes with a given alignment.
do_deallocate()- the function called when an object wants to deallocate memory.
do_is_equal()- it's used to compare if two objects have the same allocator, in most cases, you can compare addresses, but if you use some allocator adapters then you might want to check some advanced tutorials on that.
- Set your custom memory resource as active for your objects and containers.
The debug resource is just a wrapper for the real memory resource. As you can see in the allocation/deallocation functions, we only log the numbers and then defer the real job to the upstream resource.
Example use case:
Above we used debug resources twice, the first one
"pool" is used for logging every allocation that is requested to the
monotonic_buffer_resource. In the output, you can see that we had two allocations and two deallocations.
There's also another debug resource
"default". This is configured as a parent of the monotonic buffer. This means that if
pool needs to allocate., then it has to ask for the memory through our
If you add three strings like here:
Then the output is different:
This time you can notice that for the third string there was no room inside our predefined small buffer and that's why the monotonic resource had to ask for "default" for another 256 bytes.
See the full code here @Coliru.
A Custom Type
Equipped with a debug resource and also some "buffer printing techniques" we can now check if our custom type work with allocators. Let's see:
Legend: in the output the dot
. means that the element of the buffer is
0. The values that are not zeros, but smaller than a space 32 are displayed as
Let's decipher the code and the output:
The vector contains
SimpleProduct objects which is just a string and a number. We reserve four elements, and you can notice that our debug resource logged allocation of 160 bytes. After inserting three elements, we can spot
car and the number
7 (this is why I used
char as a price type). And then
9. We can also notice
4 as a price for the third element, but there's no name there. It means that it was allocated somewhere else.
Live code @Coliru
Allocator Aware Type
Making a custom type allocator aware is not super hard, but we have to remember about the following things:
pmr::*types when possible so that you can pass them an allocator.
allocator_typeso that allocator trait can "recognise" that your type uses allocators. You can also declare other properties for allocator traits, but in most cases, defaults will be fine.
- Declare constructor that takes an allocator and pass it further to your members.
- Declare copy and move constructors that also takes care of allocators.
- Same with assignment and move operations.
This means that our relatively simple declaration of custom type has to grow:
And here's a sample test code:
Sample code @Coliru
In the output, the first memory allocation - 144 - is for the
vector.reserve(3) and then we have another one for a longer string (3rd element). The full buffer is also printed (code available in the Coliru link) that shows the place where the string is located.
"Full" Custom Containers
Our custom object was composed of other
pmr:: containers, so it was much more straightforward! And I guess in most cases you can leverage existing types. However, if you need to access allocator and perform custom memory allocations, then you should see Pablo's talk where he guides through an example of a custom list container.
In this blog post, we've made another journey inside deep levels of the Standard Library. While allocators are something terrifying, it seems that with polymorphic allocator things get much more comfortable. This happens especially if you stick with lots of standard containers that are exposed in the
Let me know what's your experience with allocators and
pmr:: stuff. Maybe you implement your types differently? (I tried to write correct code, but still, some nuances are tricky. Let's learn something together :)
Published at DZone with permission of Bartłomiej Filipek , DZone MVB. See the original article here.
Opinions expressed by DZone contributors are their own.