I have a class hierarchy that I'm storing in a std::vector<std::unique_ptr<Base>>
. There is frequent adding and removing from this vector, so I wanted to experiment with custom memory allocation to avoid all the calls to new
and delete
. I'd like to use STL tools only, so I'm trying std::pmr::unsynchronized_pool_resource
for the allocation, and then adding a custom deleter to the unique_ptr
.
Here's what I've come up with so far:
#include <memory_resource>
#include <vector>
#include <memory>
// dummy classes
struct Base
{
virtual ~Base() {}
};
struct D1 : public Base
{
D1(int i_) : i(i_) {}
int i;
};
struct D2 : public Base
{
D2(double d_) : d(d_) {}
double d;
};
// custom deleter: this is what I'm concerned about
struct Deleter
{
Deleter(std::pmr::memory_resource& m, std::size_t s, std::size_t a) :
mr(m), size(s), align(a) {}
void operator()(Base* a)
{
a->~Base();
mr.get().deallocate(a, size, align);
}
std::reference_wrapper<std::pmr::memory_resource> mr;
std::size_t size, align;
};
template <typename T>
using Ptr = std::unique_ptr<T, Deleter>;
// replacement function for make_unique
template <typename T, typename... Args>
Ptr<T> newT(std::pmr::memory_resource& m, Args... args)
{
auto aPtr = m.allocate(sizeof(T), alignof(T));
return Ptr<T>(new (aPtr) T(args...), Deleter(m, sizeof(T), alignof(T)));
}
// simple construction of vector
int main()
{
auto pool = std::pmr::unsynchronized_pool_resource();
auto vec = std::vector<Ptr<Base>>();
vec.push_back(newT<Base>(pool));
vec.push_back(newT<D1>(pool, 2));
vec.push_back(newT<D2>(pool, 4.0));
return 0;
}
This compiles, and I'm pretty sure that it doesn't leak (please tell me if I'm wrong!) But I'm not too happy with the Deleter
class, which has to take extra arguments for the size and alignment.
I first tried making it a template, so that I could work out the size and alignment automatically:
template <typename T>
struct Deleter
{
Deleter(std::pmr::memory_resource& m) :
mr(m) {}
void operator()(Base* a)
{
a->~Base();
mr.get().deallocate(a, sizeof(T), alignof(T));
}
std::reference_wrapper<std::pmr::memory_resource> mr;
};
But then the unique_ptrs
for each type are incompatible, and the vector won't hold them.
Then I tried deallocating through the base class:
mr.get().deallocate(a, sizeof(Base), alignof(Base));
But this is clearly a bad idea, as the memory that's deallocated has a different size and alignment from what was allocated.
So, how do I deallocate through the base pointer without storing the size and alignment at runtime? delete
seems to manage, so it seems like it should be possible here as well.
CodePudding user response:
After writing my answer, I would recommend you stick with your code.
Letting unique_ptr
handle the storage is not bad at all, it is allocated on stack if unique_ptr
itself is, it is safe, and there is no additional overhead at deallocation time. The latter is not true for std::shared_ptr
which uses type-erause for its deleters.
I think it is the cleanest and simplest way how to achieve the goal. And there's nothing wrong with your code as far as I can tell.
Most allocators to my knowledge allocate extra space for storing any data they need for deallocation directly next to the pointer they return to you. We can do the same to the aPtr
blob:
// Extra information needed for deallocation
struct Header {
std::size_t s;
std::size_t a;
std::pmr::memory_resource* res;
};
// Deleter is now just a free function
void deleter(Base* a) {
// First delete the object itself.
a->~Base();
// Obtain the header
auto* ptr = reinterpret_cast<unsigned char*>(a);
Header* header = reinterpret_cast<Header*>(ptr - sizeof(Header));
// Deallocate the allocated blob.
header->res->deallocate(ptr, header->s, header->a);
};
// Use the new custom function.
template <typename T>
using Ptr = std::unique_ptr<T, decltype(&deleter)>;
template <typename T, typename... Args>
Ptr<T> newT(std::pmr::memory_resource& m, Args... args) {
// Let the compiler calculate the correct way how to store `T` and `H`
// together.
struct Storage {
Header header;
T type;
};
Header h = {sizeof(Storage), alignof(Storage)};
auto aPtr = m.allocate(h.s, h.a);
// Use dummy header.
Storage* storage = new (aPtr) Storage{h, T(args...)};
static_assert(sizeof(Storage) == (sizeof(Header) sizeof(T)),
"No padding bytes allowed in Storage.");
return Ptr<T>(&storage->type, deleter);
}
We store all information necessary for deallocation in Header
structure.
Allocating both T
and the header in a single blob is not straight forward as it might seem - see below. We need at least sizeof(T) sizeof(Header)
bytes but must also respect the alignof(T)
. So we let the compiler figure it out via Storage
.
This way we can allocate T
properly and return a pointer to &storage->type
to the user. The issue now is that there might be some to-deleter
-unknown amount of padding in Storage
between header
and type
, thus the deleter
function would not be able to recover &storage->header
only from &storage->type
pointer.
I have two proposals for this:
- Just assert the padding amount to 0.
- Manually write the header at the known place, albeit I cannot guarantee 100% safe.
Restricting to known padding
Although the extra padding in Storage
is unlikely because Header
is aligned to 8 bytes on normal 64-bit systems which should be generally enough for all T
s, there is no such alignment guarantee in C . vtable
pointer makes this even less guaranteed IMHO and the fact that alignas(N)
offers some user-control over the alignment, increasing it in particular for e.g. vector instructions, doesn't help either. So to be safe, we can just use static_assert
and if any "weird" type comes along, the code will not compile and remain safe.
If that happens, one can manually add extra padding to Storage
and modify the subtraction amount. The cost would be extra memory for that padding for all allocations.
Writing the header manually
Another option is that we just ignore storage->header
member and write the header ourselves directly before type
, potentially into the padding area. This requires the use of memcopy
because we cannot just placement-new
it there because of possible alignof(Header)
mismatch. Same in deleter
itself because there is no Header
object at ptr-sizeof(Header)
, simple reinterpret_cast<Header*>(ptr-sizeof(header))
would break the strict aliasing rule.
// Extra information needed for deallocation
struct Header {
std::size_t s;
std::size_t a;
std::pmr::memory_resource* res;
};
// Deleter is now just a free function
void deleter(Base* a) {
// First delete the object itself.
a->~Base();
// Obtain the header
auto* ptr = reinterpret_cast<unsigned char*>(a);
Header header;
std::memcpy(&header, ptr - sizeof(Header), sizeof(Header));
// Deallocate the allocated blob.
header.res->deallocate(ptr, header.s, header.a);
};
// Use the new custom function.
template <typename T>
using Ptr = std::unique_ptr<T, decltype(&deleter)>;
template <typename T, typename... Args>
Ptr<T> newT(std::pmr::memory_resource& m, Args... args) {
// Let the compiler calculate the correct way how to store `T` and `H`
// together.
struct Storage {
Header header;
// Padding???
T type;
};
Header h = {sizeof(Storage), alignof(Storage)};
auto aPtr = m.allocate(h.s, h.a);
// Use dummy header.
Storage* storage = new (aPtr) Storage{{0, 0}, T(args...)};
// Write our own header at the known -sizeof(Header) offset.
auto* ptr = reinterpret_cast<unsigned char*>(storage);
std::memcpy(ptr - sizeof(Header), &h, sizeof(Header));
return Ptr<T>(&storage->type, deleter);
}
I know this solution is safe w.r.t strict aliasing, object lifetime and allocating T
. What I am not 100% certain about is whether the compiler is allowed to store anything relevant to T
inside the potential padding bytes, which would thus be overwritten by the manually-written header.