DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Refcards Trend Reports
Events Video Library
Refcards
Trend Reports

Events

View Events Video Library

Related

  • Singleton: 6 Ways To Write and Use in Java Programming
  • Exploring Exciting New Features in Java 17 With Examples
  • Generics in Java and Their Implementation
  • High-Performance Java Serialization to Different Formats

Trending

  • When Snowflake Lies to You: Understanding False Failures in dbt Pipelines
  • MuleSoft IDP: Enhancing Efficiency and Accuracy in Data Extraction
  • No More Cheap Claude: 4 First Principles of Token Economics in 2026
  • A Walk-Through of the DZone Article Editor
  1. DZone
  2. Coding
  3. Languages
  4. 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.

By 
Bartłomiej Filipek user avatar
Bartłomiej Filipek
·
Aug. 26, 20 · Tutorial
Likes (4)
Comment
Save
Tweet
Share
17.3K Views

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.

The Goal

In the previous article there was similar code:

Java
 




x


 
1
char buffer[256] = {}; // a small buffer on the stack
2
std::fill_n(std::begin(buffer), std::size(buffer) - 1, '_');
3

           
4
std::pmr::monotonic_buffer_resource pool{std::data(buffer),
5
                                         std::size(buffer)};
6
std::pmr::vector<std::pmr::string> vec{ &pool };
7
// ...



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.

Since the 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:

Java
 




xxxxxxxxxx
1


 
1
struct Product {
2
    std::string name;
3
    char cost { 0 }; // for simplicity
4
};



If I plug in this into the vector:

Java
 




xxxxxxxxxx
1


 
1
std::pmr::vector<Product> prods { &pool };



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.

References

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::allocator was 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 *_pool_resource works.
  • 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 std::pmr::memory_resource
  • Implement:
    • 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.

Here's a code based on Sticky Bits and Pablo Halpern's talk.

Java
 




xxxxxxxxxx
1
24


 
1
class debug_resource : public std::pmr::memory_resource {
2
public:
3
    explicit debug_resource(std::string name, 
4
       std::pmr::memory_resource* up = std::pmr::get_default_resource())
5
        : _name{ std::move(name) }, _upstream{ up } 
6
    { }
7

           
8
    void* do_allocate(size_t bytes, size_t alignment) override {
9
        std::cout << _name << " do_allocate(): " << bytes << '\n';
10
        void* ret = _upstream->allocate(bytes, alignment);
11
        return ret;
12
    }
13
    void do_deallocate(void* ptr, size_t bytes, size_t alignment) override {
14
        std::cout << _name << " do_deallocate(): " << bytes << '\n';
15
        _upstream->deallocate(ptr, bytes, alignment);
16
    }
17
    bool do_is_equal(const std::pmr::memory_resource& other) const noexcept override {
18
        return this == &other;
19
    }
20

           
21
private:
22
    std::string _name;
23
    std::pmr::memory_resource* _upstream;
24
};



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:

Java
 




xxxxxxxxxx
1
11


 
1
constexpr size_t BUF_SIZE = 128;
2
char buffer[BUF_SIZE] = {}; // a small buffer on the stack
3
std::fill_n(std::begin(buffer), std::size(buffer) - 1, '_');
4

           
5
debug_resource default_dbg { "default" };
6
std::pmr::monotonic_buffer_resource pool{std::data(buffer), std::size(buffer), &default_dbg};
7
debug_resource dbg { "pool", &pool };
8
std::pmr::vector<std::string> strings{ &dbg };
9

           
10
strings.emplace_back("Hello Short String");
11
strings.emplace_back("Hello Short String 2");



The output:

Java
 




xxxxxxxxxx
1


 
1
pool do_allocate(): 32
2
pool do_allocate(): 64
3
pool do_deallocate(): 32
4
pool do_deallocate(): 64



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 "default" object.:

If you add three strings like here:

Java
 




xxxxxxxxxx
1


 
1
strings.emplace_back("Hello Short String");
2
strings.emplace_back("Hello Short String 2");
3
strings.emplace_back("Hello A bit longer String");



Then the output is different:

Java
 




xxxxxxxxxx
1


 
1
pool do_allocate(): 32
2
pool do_allocate(): 64
3
pool do_deallocate(): 32
4
pool do_allocate(): 128
5
default do_allocate(): 256
6
pool do_deallocate(): 64
7
pool do_deallocate(): 128
8
default do_deallocate(): 256



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:

Java
 




xxxxxxxxxx
1
33


 
1
struct SimpleProduct {
2
    std::string _name;
3
    char _price { 0 };
4
};
5

           
6
int main() {
7
    constexpr size_t BUF_SIZE = 256;
8
    char buffer[BUF_SIZE] = {}; // a small buffer on the stack
9
    std::fill_n(std::begin(buffer), std::size(buffer) - 1, '_');
10

           
11
    const auto BufferPrinter = [](std::string_view buf, std::string_view title) { 
12
        std::cout << title << ":\n";
13
        for (size_t i = 0; i < buf.size(); ++i) {
14
            std::cout << (buf[i] >= ' ' ? buf[i] : '#');
15
            if ((i+1)%64 == 0) std::cout << '\n';
16
        }
17
        std::cout << '\n';
18
    };
19

           
20
    BufferPrinter(buffer, "initial buffer");
21

           
22
    debug_resource default_dbg { "default" };
23
    std::pmr::monotonic_buffer_resource pool{std::data(buffer), std::size(buffer), &default_dbg};
24
    debug_resource dbg { "buffer", &pool };
25
    std::pmr::vector<SimpleProduct> products{ &dbg };
26
    products.reserve(4);
27

           
28
    products.emplace_back(SimpleProduct { "car", '7' }); 
29
    products.emplace_back(SimpleProduct { "TV", '9' }); 
30
    products.emplace_back(SimpleProduct { "a bit longer product name", '4' }); 
31

           
32
    BufferPrinter(std::string_view {buffer, BUF_SIZE}, "after insertion");
33
}



Possible output:

Java
 




xxxxxxxxxx
1
11


 
1
________________________________________________________________
2
________________________________________________________________
3
________________________________________________________________
4
_______________________________________________________________
5
buffer do_allocate(): 160
6
after insertion:
7
p"---•..-.......car.er..-~---•..7_______-"---•..-.......TV..er..
8
-~---•..9_______0-j-....-.......-.......________4_______________
9
________________________________________________________________
10
_______________________________________________________________.
11
buffer do_deallocate(): 160



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 TV with 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:

  • Use pmr::* types when possible so that you can pass them an allocator.
  • Declare allocator_type so 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:

Java
 




xxxxxxxxxx
1
22


 
1
struct Product {
2
    using allocator_type = std::pmr::polymorphic_allocator<char>;
3

           
4
    explicit Product(allocator_type alloc = {}) 
5
    : _name { alloc } { }
6

           
7
    Product(std::pmr::string name, char price, 
8
            const allocator_type& alloc = {}) 
9
    : _name { std::move(name), alloc }, _price { price } { }
10

           
11
    Product(const Product& other, const allocator_type& alloc) 
12
    : _name { other._name, alloc }, _price { other._price } { }
13

           
14
    Product(Product&& other, const allocator_type& alloc) 
15
    : _name{ std::move(other._name), alloc }, _price { other._price } { }
16

           
17
    Product& operator=(const Product& other) = default;
18
    Product& operator=(Product&& other) = default;
19

           
20
    std::pmr::string _name;
21
    char _price { '0' };
22
};


And here's a sample test code:

Java
 




xxxxxxxxxx
1
10


 
1
debug_resource default_dbg { "default" };
2
std::pmr::monotonic_buffer_resource pool{std::data(buffer), 
3
                       std::size(buffer), &default_dbg};
4
debug_resource dbg { "buffer", &pool };
5
std::pmr::vector<Product> products{ &dbg };
6
products.reserve(3);
7

           
8
products.emplace_back(Product { "car", '7', &dbg }); 
9
products.emplace_back(Product { "TV", '9', &dbg }); 
10
products.emplace_back(Product { "a bit longer product name", '4', &dbg }); 



The output:

Java
 




xxxxxxxxxx
1


 
1
buffer do_allocate(): 144
2
buffer do_allocate(): 26
3
after insertion:
4
-----•..-----•..-.......car.#•..-.......7_______-----•..-----•..
5
-.......TV..#•..-.......9_______-----•..@----•..-.......-.......
6
________4_______a bit longer product name.______________________
7
_______________________________________________________________.
8
buffer do_deallocate(): 26
9
buffer do_deallocate(): 144



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.

CppCon 2017: Pablo Halpern "Allocators: The Good Parts" - YouTube

Summary

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 pmr:: namespace.

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 :)

Allocator (C++) Debug (command) Memory (storage engine) Java (programming language) c++ Strings Container Data Types Object (computer science)

Published at DZone with permission of Bartłomiej Filipek. See the original article here.

Opinions expressed by DZone contributors are their own.

Related

  • Singleton: 6 Ways To Write and Use in Java Programming
  • Exploring Exciting New Features in Java 17 With Examples
  • Generics in Java and Their Implementation
  • High-Performance Java Serialization to Different Formats

Partner Resources

×

Comments

The likes didn't load as expected. Please refresh the page and try again.

  • RSS
  • X
  • Facebook

ABOUT US

  • About DZone
  • Support and feedback
  • Community research

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Core Program
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 3343 Perimeter Hill Drive
  • Suite 215
  • Nashville, TN 37211
  • [email protected]

Let's be friends:

  • RSS
  • X
  • Facebook