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

Viewed 140

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;
}
2 Answers

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

Here is a wrapper I wrote around the specialization of std::unique_ptr<T, boost::alloc_deleter>. The unique pointer returned by boost::allocate_unique is implicitly convertible to the wrapper. The wrapper is default constructible, move-assignable and also has .get() return a raw pointer instead of a boost fancy pointer type (which requires an additional .ptr() to get a raw pointer).

The only downside is that you have to use the wrapper explicitly instead of e.g. auto with boost::allocate_unique.

using Pma = std::pmr::polymorphic_allocator<std::byte>;
template<typename T> using pmr_deleter = boost::alloc_deleter<T, Pma>;
template<typename T> class pmr_unique_ptr : public std::unique_ptr<T, pmr_deleter<T>> {
  public:
    using std::unique_ptr<T, pmr_deleter<T>>::unique_ptr;
    T* get() const { return std::unique_ptr<T, pmr_deleter<T>>::get().ptr(); }
    pmr_unique_ptr() : std::unique_ptr<T, pmr_deleter<T>>(nullptr, pmr_deleter<T>(std::pmr::null_memory_resource())) { }
    pmr_unique_ptr(decltype(nullptr)) : pmr_unique_ptr() { }
    template<typename P>
    pmr_unique_ptr(std::unique_ptr<P, pmr_deleter<P>>&& p)
        : pmr_unique_ptr(static_cast<T*>(p.get().ptr()), *reinterpret_cast<pmr_deleter<T>*>(&p.get_deleter())) {
        p.release();
    }
    template<>
    pmr_unique_ptr(std::unique_ptr<T, pmr_deleter<T>>&& p) : std::unique_ptr<T, pmr_deleter<T>>(std::move(p)) { };
    pmr_unique_ptr(T* p, pmr_deleter<T> d) :  std::unique_ptr<T, pmr_deleter<T>>(boost::detail::sp_alloc_ptr<T,T *>(1, p), d) { };
    pmr_unique_ptr(const pmr_unique_ptr&) = delete;
    pmr_unique_ptr(pmr_unique_ptr&& p) : std::unique_ptr<T, pmr_deleter<T>>(std::move(p)) { }
    template<typename P> operator pmr_unique_ptr<P>() {
        P* basep = static_cast<P*>(get());
        pmr_deleter<P> d(*reinterpret_cast<pmr_deleter<P>*>(&this->get_deleter()));
        this->release();
        return {basep, std::move(d)};
    }
    pmr_unique_ptr& operator=(pmr_unique_ptr&& other) {
        this->std::unique_ptr<T, pmr_deleter<T>>::~unique_ptr();
        new (static_cast<std::unique_ptr<T, pmr_deleter<T>>*>(this)) std::unique_ptr<T, pmr_deleter<T>>(std::move(other));
        return *this;
    }
    template<typename P> pmr_unique_ptr& operator=(std::unique_ptr<P, pmr_deleter<P>>&& p) {
        return operator=(pmr_unique_ptr(pmr_unique_ptr<P>(std::move(p))));
    }
};

Example which compiles:

#include <memory_resource>
#include <boost/smart_ptr/allocate_unique.hpp>

// ... the definitions from above
// ...

  pmr_unique_ptr<int> p;
  pmr_unique_ptr<int> p2 = nullptr;

  p2 = boost::allocate_unique<int>(Pma(std::pmr::new_delete_resource()), 5);
  p = std::move(p2);
  int *rawp = p.get();
Related