I have a question about allocate_unique
from Boost. It looks like that resulting unique_ptr
s are quite limited - they cannot be default constructed to nullptr
without providing a deleter (even an invalid one), and also, move assignment does not work.
Luckily, move construction does work, so I was able to hack around not having move assignment by calling the destructor and move-constructing using placement new.
Is it a defect in Boost that alloc_deleter
is not moveable, and thus disables move assignment of these unique_ptr
s? Or am I misunderstanding something?
#include <memory>
#include <memory_resource>
#include <boost/smart_ptr/allocate_unique.hpp>
#include <iostream>
using Pma = std::pmr::polymorphic_allocator<std::byte>;
template<typename T> using pmr_deleter = boost::alloc_deleter<T, Pma>;
template<typename T> using pmr_unique_ptr = std::unique_ptr<T, pmr_deleter<T>>;
struct Vertex {
float x = 1;
float y = 2;
float z = 3;
};
int main() {
auto& res = *std::pmr::new_delete_resource();
pmr_deleter<Vertex> d(nullptr);
pmr_unique_ptr<Vertex> v_empty(nullptr, d); // will not default construct without deleter??
pmr_unique_ptr<Vertex> v = boost::allocate_unique<Vertex>(Pma(&res), Vertex{7,8,9});
// v_empty = std::move(v); // operator=(unique_ptr&&) gets deleted because `alloc_deleter` is not moveable!
// We can hack in a move like this:
v_empty.~pmr_unique_ptr<Vertex>();
new (&v_empty) pmr_unique_ptr<Vertex>(v.get(), v.get_deleter());
v.release();
std::cout << v_empty->x << "," << v_empty->y << "," << v_empty->z << std::endl;
return 0;
}
Polymorphic allocators are stateful, which means they cannot be default-constructed - because they wouldn't know about the memory resource they're supposed to work with.
This is not particular to PMR or unique pointers, it will also crop up when e.g. using Boost Interprocess allocators on a vector
- you will always have to pass an initializer for the allocator.
Ifff you want a global/singleton memory resource, you can obviously declare a custom deleter that encodes that constant:
template <typename T> struct pmr_deleter : boost::alloc_deleter<T, Pma> {
pmr_deleter()
: boost::alloc_deleter<T, Pma>(std::pmr::new_delete_resource()) {}
};
This would allow the default constructor(s) to work:
pmr_unique_ptr<Vertex> v_empty; // FINE
pmr_unique_ptr<Vertex> v_empty(nullptr); // ALSO FINE
However it comes at the cost of no longer being type-compatible with the allocate_unique
factory return type (alloc_deleter
).
You can probably pave hack this, but I think it would probably be best to understand the situation before you decide whether that's worth it. (Hint: I don't think it is, because it is precisely the goal of PMR to type erase the allocator difference, trading in runtime state instead. If you go and move all state into the allocator again, you effectively made it a static allocator again, which is where we would have been without PMR anyways)
pmr_deleter<Vertex> d(nullptr);
Is ill-formed, as the argument may never be null. Both the compiler will warn about this at -Wnon-null
, just as Asan/UBSan will:
/home/sehe/Projects/stackoverflow/test.cpp:18:34: runtime error: null pointer passed as argument 2, which is declared to never be null