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:
Default constructed, in which case it uses
std::pmr::get_default_resource()
return value, which is amemory_resource*
.Pass it a
memory_resource*
.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_resource
s 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 amemory_resource
rather than a reference to amemory_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_resource
s. 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
Using 3Rd Party Header Files with Rcpp
Why How to Use 'Std::Move' on a 'Const' Object
Template Function as a Template Argument
Why Does This Program Crash: Passing of Std::String Between Dlls
Making a Template Parameter a Friend
Doing a Static_Assert That a Template Type Is Another Template
Does Constraint Subsumption Only Apply to Concepts
"Symbol(S) Not Found for Architecture X86_64" on Qtcreator Project
How to Make Std::Make_Unique a Friend of My Class
#Include Errors Detected in VScode
Embed Resources (Eg, Shader Code; Images) into Executable/Library with Cmake
Linking to Msvc Dll from Mingw
"If" Argument Evaluation Order