glades
glades

Reputation: 4789

Deep-copying unique_ptrs to pmr resources (using pmr allocators)

I rewrote my library based on polymorphic memory resources. I think they're amazing, however there are edge cases that are not really covered by the standard.

For example just now I have the following problem: A class type holds an unique_ptr to a pmr-allocated resource. Now, how to deal with this in the copy constructor? The underlying pmr-resource (a pmr::vector) will have to be constructed using the pmr-resource (which is passed down automatically using the pmr allocators I think). So I'm using the new_object() and delete_object() facilities of the allocator to feed my unique ptr. But it fails because 1) allocators are not delete-functors (don't provide operator() overload for delete) and/or 2) I can't use a function directly because those functions are non-static. How do I resolve this?

Demo

#include <memory_resource>
#include <memory>

struct profile
{
    using allocator_type = std::pmr::polymorphic_allocator<std::byte>;
    profile(allocator_type allocator = {})
        :   allocator_{allocator}
    {}

    profile(const profile& other, allocator_type allocator = {})
        :   allocator_{ allocator }
    {
        // Deep copy vector (how to do this?)
        auto f = std::pmr::polymorphic_allocator<std::byte>::delete_object;
        vals_ = std::unique_ptr<profile, decltype(&f)>{ allocator_.new_object<profile>(), f };
    }

    allocator_type get_allocator() {
        return allocator_;
    }

    allocator_type allocator_;
    std::unique_ptr<std::pmr::vector<double>> vals_;
};


int main()
{
    profile p;

    profile o{p};
}

Error:

<source>:15:62: error: call to non-static member function without an object argument
        auto f = std::pmr::polymorphic_allocator<std::byte>::delete_object;
                 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^~~~~~~~~~~~~
1 error generated.

Upvotes: 1

Views: 777

Answers (1)

MichalK
MichalK

Reputation: 126

#include <memory_resource>
#include <memory>

struct profile
{
    using allocator_type = std::pmr::polymorphic_allocator<std::byte>;
    
    // This is a method
    // std::pmr::polymorphic_allocator<std::byte>::delete_object<std::pmr::vector<double>>
    // so if you want the unique_ptr to have deleter use this method you must
    // also provide it the polymorphic_allocator that it should call this method on
    // BUT
    // you don't actually even need a unique_ptr to solve the problem
public:    
    
    profile(allocator_type allocator = {})
        : allocator_{allocator}
    {}

    profile(const profile& other, allocator_type allocator = {})
        : allocator_{ allocator }
        , vals_(std::begin(other.vals_), std::end(other.vals_), allocator_)
    {
    }

    allocator_type get_allocator() {
        return allocator_;
    }

    allocator_type allocator_;
    std::pmr::vector<double> vals_;
};


int main()
{
    profile p;

    profile o{p};
}

The unique_ptr was redundant as it was just doing ptr->ptr->actual data Now it is ptr->data. Regarding move/copy construction -> copy construction using the same instance of allocator will behave correctly (std::vector is allocator aware container) same will go for different instance of allocator (second constructor) -> you're delegating the tedious work of implementing Allocator Aware Container (https://en.cppreference.com/w/cpp/named_req/AllocatorAwareContainer -> https://www.youtube.com/watch?v=kSWfushlvB8) to the actual container in the class -> std::vector Obviously std::move for instances of profile (or even instances of internal vector) would be handled correctly.

If you have a use case that would require that unique_ptr it would become a lot more complicated. unique_ptr has a following template signature:

template<class T, class Deleter>
class unique_ptr{[...]};

It doesn't do any type erasure so every time you would pass around instance of this class it would require you to provide the deleter as a template argument

This is different in case of shared_ptr which has following signature:

template<class T>
class shared_ptr{[...]};

Even though there is no template argument corresponding to deleter it allows usage of them (https://en.cppreference.com/w/cpp/memory/shared_ptr/shared_ptr)

So assuming this use case you would need to create a function like object of known type that would be the the Deleter type argument, however this comes with several disadvantages:

  • Requires you to implement AllocatorAwareContainer
  • Massively complicates the logic of the class
  • Isn't a good code practice (as you are merging two functionalities inside class that was probably not targeted to be specifically a container of arbitrary data)

Upvotes: 2

Related Questions