Polymorphic_Allocator: When and Why Should I Use It

polymorphic_allocator: when and why should I use it?

Choice quote from cppreference:

This runtime polymorphism allows objects using polymorphic_allocator to behave as if they used different allocator types at run time despite the identical static allocator type

The issue with "regular" allocators is that they change the type of the container. If you want a vector with a specific allocator, you can make use of the Allocator template parameter:

auto my_vector = std::vector<int,my_allocator>();

The problem now is that this vector is not the same type as a vector with a different allocator. You can't pass it to a function which requires a default-allocator vector, for example, or assign two vectors with a different allocator type to the same variable / pointer, eg:

auto my_vector = std::vector<int,my_allocator>();
auto my_vector2 = std::vector<int,other_allocator>();
auto vec = my_vector; // ok
vec = my_vector2; // error

A polymorphic allocator is a single allocator type with a member that can define the allocator behaviour via dynamic dispatch rather than through the template mechanism. This allows you to have containers which use specific, customised allocation, but which are still of a common type.

The customisation of allocator behavior is done by giving the allocator a std::memory_resource *:

// define allocation behaviour via a custom "memory_resource"
class my_memory_resource : public std::pmr::memory_resource { ... };
my_memory_resource mem_res;
auto my_vector = std::pmr::vector<int>(0, &mem_res);

// define a second memory resource
class other_memory_resource : public std::pmr::memory_resource { ... };
other_memory_resource mem_res_other;
auto my_other_vector = std::pmr::vector<int>(0, &mes_res_other);

auto vec = my_vector; // type is std::pmr::vector<int>
vec = my_other_vector; // this is ok -
// my_vector and my_other_vector have same type

The main remaining issue, as I see it, is that a std::pmr:: container is still not compatible with the equivalent std:: container using the default allocator. You need to make some decisions at the time you design an interface which works with a container:

  • is it likely that the container passed in may require custom allocation?
  • if so, should I add a template parameter (to allow for arbitrary allocators) or should I mandate the use of a polymorphic allocator?

A template solution allows for any allocator, including a polymorphic allocator, but has other drawbacks (generated code size, compile time, code must be exposed in header file, potential for further "type contamination" which keeps pushing the problem outward). A polymorphic allocator solution on the other hand dictates that a polymorphic allocator must be used. This precludes using std:: containers which use the default allocator, and might have implications for interfacing with legacy code.

Compared to a regular allocator, a polymorphic allocator does have some minor costs, such as the storage overhead of the memory_resource pointer (which is most likely negligible) and the cost of virtual function dispatch for allocations. The main problem, really, is probably lack of compatibility with legacy code which doesn't use polymorphic allocators.

What is the purpose and usage of `memory_resource`?

A polymorphic_allocator is intended to let you have an allocator whose behavior is dynamically determined at runtime.

The only way to create a polymorphic_allocator is:

  1. Default constructed, in which case it uses std::pmr::get_default_resource() return value, which is a memory_resource*.

  2. Pass it a memory_resource*.

  3. copy from another polymorphic_allocator.

So the point of customization for a polymorphic_allocator is creating a class that inherits from memory_resource and implementing its methods, or using one of the pre-declared memory_resources that are defined in std::pmr: (un)synchronized_pool_resource and monotonic_buffer_resource as types, or std::pmr::new_delete_resource() / std::pmr::null_memory_resource().

Suppose you want a your memory to be allocated using a strategy different than the 5 provided in std::pmr. Then you make a class that inherits from std::pmr::memory_resource, and pass it to a container which uses a polymorphic_allocator.

Why does `polymorphic_allocator` take a `memory_resource` pointer and not a reference?

N3916:

Note that the memory-resource library is designed so that the ShoppingList constructor accepts a pointer to a memory_resource rather than a reference to a memory_resource. It was noted that one common practice is to use references rather than pointers in situations where a null pointer is out of contract. However, there is a more compelling practice of avoiding constructors that take objects by reference and store their addresses. We also want to avoid passing non-const references, as that, too, is usually considered bad practice (except in overloaded operators).

Should new C++ code use memory resources instead of allocators?

At this point no.

Allocators in C++ currently are much easier than they used to be.

They provide both pmr (polymorphic) and classic allocator support.

More importantly, pmr based allocation has not been in heavy use for years. Any weaknesses may still come to light.

Fast pool based allocators, or even fixed buffer ones or sbo (small buffer optimization) extensions, may notice the virtualization overhead.

Understanding std::pmr::new_delete_resource

NicolBolas already gave a pretty clear answer, but since there appears to be some confusion in the comments regarding the "static storage duration" which I feels needs some clarity.

The function called new_delete_resource() is defined, on cppreference, to have the following affect:

Return Value:

Returns a pointer p to a static storage duration object of a type derived from std::pmr::memory_resource, with the following properties:

  • its allocate() function uses ::operator new to allocate memory;
  • its deallocate() function uses ::operator delete to deallocate memory;
  • for any memory_resource r, p->is_equal(r) returns &r == p.

The same value is returned every time this function is called.

(Emphasis mine)

What this means is that the std::pmr::memory_resource object returned from this function has static-storage duration; not that calls of allocate() operate on static storage duration.

For example, this may be implemented as:

namespace std::pmr {

memory_resource* new_delete_resource() {
static internal_new_delete_resource s_new_delete_resource;

return &s_new_delete_resource;
}

} // namespace std::pmr

To be clear: the following code does not allocate memory with static-storage duration (by default[1]):

auto* p = new_delete_resource()->allocate(...);

Rather, the above code is actually roughly equivalent to writing:

auto* p = new char[...];

The "global operators" that the documentation refers to are the functions ::operator new and ::operator delete.


[1] By default, these operate on the heap -- though ::operator new and ::operator delete can be overridden by the user if they choose to define these. A program is legally allowed to define their own allocation mechanism if they choose -- at which point this may, in fact, be static-storage duration. However, such a point is more esoteric; as far as the standard is concerned, the storage duration of the pointer is dynamic -- and not explicitly static storage.

So what's the point?

Aside from being a suitable and useful default for an allocator (e.g. the default_resource), this also offers great composability with other memory_resources. For example, a pool_resource may use this as the upstream memory_resource for when the pool runs out of allocations.

Having a resource like this becomes really important for symmetry with std::allocator<T> (which means a cheap upgrade path), and for enabling std::pmr::polymorphic_allocator to have a suitable default.

Why new and delete?

new and delete are built-in facilities in C++ -- so it's easy to use these and base it in terms of this. Since the global ::operator new and ::operator delete can be overridden by custom definitions, this makes it a simple customization point that works seamlessly in any existing application. Additionally, it follows the existing pattern for std::allocator<T> which used new and delete.

If, instead, this were std::malloc/std::free, or some other allocation function -- then old user code that previously defined custom hooks for ::operator new would cease to function correctly. This would lead the behavior of a container using std::allocator<T> to behave differently than a container using std::pmr::polymorphic_allocator<T> with a std::pmr::new_delete_resource -- which would be undesirable. (Note: the default resource is a new_delete_resource, which is what provides this symmetry by default).

C++: Can't propagate polymorphic_allocator with scoped_allocator_adaptor

Argh. The explanation is hidden in std::experimental::pmr::polymorphic_allocator::construct:

This function is called (through std::allocator_traits) by any
allocator-aware object, such as std::vector, that was given a
std::polymorphic_allocator as the allocator to use. Since
memory_resource* implicitly converts to polymorphic_allocator, the
memory resource pointer will propagate to any allocator-aware
subobjects using polymorphic allocators.

So it turns out that polymorphic allocators automatically propagate. That also explains why the allocator is passed twice in the gcc error message.

Here is a working version:

template <typename T>
using Alloc = std::experimental::pmr::polymorphic_allocator<T>;

template <typename T>
using PmrVector = std::vector<T, Alloc<T>>;

using Inner = PmrVector<int>;

int main() {
MemoryResource resource{};

PmrVector<Inner> v(1000, Alloc<Inner>{&resource});
v[0].resize(100);
}

And here is the information that I would have need a couple of hours ago:

How do I use polymorphic_allocator and scoped_allocator_adaptor together?

You don't. Make sure that all inner containers also use polymorphic allocators, then the memory resource will be handed down automatically.

Should I call `delete` on object allocated using polymorphic allocator

A polymorphic allocator is a cheaply copyable object and doesn't own objects.

What you might be confusing it with is a memory_resource, which has capacity to store objects. Still, it doesn't own those, because it cannot even know the type(s) of object(s) stored in its capacity.

On the other hand, there are container types that use an allocator to allocate storage for their objects. The container does own the objects, and will destruct them and deallocate from the same (or rather, equivalent) allocator.

In short, you will not be allocating from an allocator, your allocator-aware container will. And it will not call delete, but rather the expectable allocator.deallocate() or allocator.delete_object() depending on how the allocation was made.

Example 1: Allocator-Aware Container

Because an example speaks a thousand words:

Live On Compiler Explorer

#include <iostream>
#include <memory_resource>
#include <vector>

struct X {
X() { std::cout << __PRETTY_FUNCTION__ << "\n"; }
~X() { std::cout << __PRETTY_FUNCTION__ << "\n"; }
};

int main() {
std::pmr::vector<X> v;
v.reserve(3); // watch v reallocate if you remove this line
v.emplace_back();
v.emplace_back();
v.emplace_back();
} // v destructs and deallocates automically

Prints:

X::X()
X::X()
X::X()
X::~X()
X::~X()
X::~X()

I'll leave it as an exercise to see whether your standard library implementation changes behaviour if you remove the call to reserve().

Example 2: Raw allocator use

It's pretty clear that the allocator interface is not for direct consumption:

Live On Compiler Explorer

#include <iostream>
#include <memory_resource>
#include <vector>

struct X {
X() { std::cout << __PRETTY_FUNCTION__ << "\n"; }
~X() { std::cout << __PRETTY_FUNCTION__ << "\n"; }
};

int main() {
std::pmr::monotonic_buffer_resource mem(1024);
std::pmr::polymorphic_allocator<X> alloc(&mem);
{
auto x1 = alloc.new_object<X>();
auto x2 = alloc.new_object<X>();
auto x3 = alloc.new_object<X>();

// cannot use `operator delete`:
// delete x1; // Undefined Behaviour

// need to manually delete the objects we own through the allocator
alloc.delete_object(x3);
alloc.delete_object(x2);
alloc.delete_object(x1);
}
{ // c++17 interface
auto x1 = alloc.allocate(sizeof(X)); alloc.construct(x1);
auto x2 = alloc.allocate(sizeof(X)); alloc.construct(x2);
auto x3 = alloc.allocate(sizeof(X)); alloc.construct(x3);

// cannot use `operator delete`:
// delete x1; // Undefined Behaviour

// need to manually delete the objects we own through the allocator
alloc.destroy(x3); alloc.deallocate(x3, sizeof(X));
alloc.destroy(x2); alloc.deallocate(x2, sizeof(X));
alloc.destroy(x1); alloc.deallocate(x1, sizeof(X));
}
}

Prints

X::X()
X::X()
X::X()
X::~X()
X::~X()
X::~X()
X::X()
X::X()
X::X()
X::~X()
X::~X()
X::~X()

polymorphic_allocator: how to change the memory resource of a container

There's no easy way to. It is intended that you cannot change the memory resource associated with a container during its lifetime. You can make a new container with the desired memory resource using the allocator-extended copy/move constructor, but not change the memory resource of existing ones.

Deleting polymorphic objects allocated with C++17 pmr memory resource

The problem

Prior to C++20, there was no way to invoke a deallocation function (operator delete) that didn't call your class' destructor first, making it impossible for you to clean up extra explicitly allocated resources owned by your class (without hacky code like your static pointer)

The solution

If you have access to C++20, then I encourage you to use destroying delete which was created to solve problems like this.

  • Your class can hold onto an instance of std::pmr::memory_resource* (injected through the constructor)
  • Change your operator delete into e.g., void operator delete(Node *ptr, std::destroying_delete_t) noexcept
    • destroying_delete is a tag that, when you use it, indicates that you will take responsibility for invoking the appropriate destructor.
  • Derived classes should also implement a similar deleter.

Without making too many changes to your code, we can do the following in Node:

struct Node {
// NOTE: this is actually not necessary..
using allocator_type = std::pmr::polymorphic_allocator<Node>;

void operator delete(Node *ptr, std::destroying_delete_t) noexcept {
deleter(ptr);
}
// don't bother with getters/setters so far..
std::pmr::string name;

template <class TNode >
static TNode *create(std::string_view name, std::pmr::memory_resource *res) {

std::pmr::polymorphic_allocator<TNode> alloc(res);
auto ptr = alloc.allocate(1);
::new (ptr) TNode(alloc, res);
ptr->name = name;
return ptr;
}
virtual ~Node() {
std::cerr << "Destructing node: " << name << std::endl;
}

protected:
Node(const allocator_type& alloc, std::pmr::memory_resource *res)
: name(alloc), s_deleterResource(res) {}

std::pmr::memory_resource *s_deleterResource = nullptr;

template<class TNode>
static void deleter(TNode* ptr) noexcept {
if (ptr->s_deleterResource != nullptr) {
auto* deleterResource = ptr->s_deleterResource;
ptr->~TNode();
std::cerr << "Deleting mem: " << ptr << " using PMR resource\n";
std::pmr::polymorphic_allocator< TNode >(deleterResource)
.deallocate(ptr, 1);
}
else {
std::cerr << "Deleting mem: " << ptr << " using default\n";
::delete ptr;
}
}
};

And then in e.g., LeafNode you can write this:

struct LeafNode : Node {
friend struct Node;
using allocator_type = std::pmr::polymorphic_allocator<LeafNode>;

void operator delete(LeafNode *ptr, std::destroying_delete_t) noexcept {
deleter(ptr);
}

protected:
explicit LeafNode(const allocator_type& alloc, std::pmr::memory_resource *res) :
Node(alloc, res), m_payload(77, alloc) { }

std::pmr::vector< uint8_t > m_payload;
};

Live Demo

Allocated: 88 bytes with 'buffered resource': 0x7ffebb5906d0
Allocated: 88 bytes with 'buffered resource': 0x7ffebb590728
Allocated: 77 bytes with 'buffered resource': 0x7ffebb590780
Allocated: 8 bytes with 'buffered resource': 0x7ffebb5907d0
Allocated: 88 bytes with 'buffered resource': 0x7ffebb5907d8
Allocated: 77 bytes with 'buffered resource': 0x7ffebb590830
Allocated: 16 bytes with 'buffered resource': 0x7ffebb590880
Deallocating 8 bytes with 'buffered resource': 0x7ffebb5907d0
Beginning tree deletetion..
Deallocating 77 bytes with 'buffered resource': 0x7ffebb590780
Destructing node: child1
Deleting mem: 0x7ffebb590728 using PMR resource
Deallocating 88 bytes with 'buffered resource': 0x7ffebb590728
Deallocating 77 bytes with 'buffered resource': 0x7ffebb590830
Destructing node: child2
Deleting mem: 0x7ffebb5907d8 using PMR resource
Deallocating 88 bytes with 'buffered resource': 0x7ffebb5907d8
Deallocating 16 bytes with 'buffered resource': 0x7ffebb590880
Destructing node: root
Deleting mem: 0x7ffebb5906d0 using PMR resource
Deallocating 88 bytes with 'buffered resource': 0x7ffebb5906d0

(Notice that the class is a little bit bigger because it holds onto a pointer instead of that pointer being static)



Related Topics



Leave a reply



Submit