Search code examples
c++boostpolymorphismallocator

boost::allocate_unique yields non-deafult constructible and non-move assignable unique_ptrs


I have a question about allocate_unique from Boost. It looks like that resulting unique_ptrs 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_ptrs? 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;
}

Solution

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

    Other Notes

    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