Search code examples
c++boostc++14c++17allocator

is there any way to adopt a memory resource for a vector?


I have started using pmr::allocators in my project, and I have seen a lot of performance boost and advantage from using them. I use allocator very similar to what I show int the simple example below:

#include <array>
#include <boost/container/flat_map.hpp>
#include <cassert>
#include <iostream>
#include <memory_resource>
#include <string>
#include <vector>

struct MessageBody {
  using map_t = boost::container::flat_map<
      char, char, std::less<char>,
      std::pmr::polymorphic_allocator<std::pair<char, char>>>;

  using vector_t = std::vector<char, std::pmr::polymorphic_allocator<char>>;

  MessageBody(std::pmr::memory_resource& mem_v,
              std::pmr::memory_resource& mem_m)
      : vec_(0, &mem_v), map_(&mem_m) {}

  vector_t vec_;
  map_t map_;
};

int main() {
  std::array<char, 1000> buffer;
  buffer.fill('\0');
  std::pmr::monotonic_buffer_resource vec_mem(buffer.data(), 500,
                                              std::pmr::null_memory_resource());
  std::pmr::monotonic_buffer_resource map_mem(buffer.data() + 500, 500,
                                              std::pmr::null_memory_resource());
  {
    MessageBody message(vec_mem, map_mem);
    message.vec_.push_back('1');
    assert(message.vec_.size() == 1);
  }

  {
    MessageBody message(vec_mem, map_mem);
    assert(message.vec_.size() == 1);   /// I want to adopt the previous class for a new class.
  }
}

my question is if there is any way I can adopt the memory_resources for another class without having the repopulate entire data inside vector and map.

the only way I can think of doing that (and I know it is a terrible idea), is to implement a new class that inherits from std::vector, that class has an adopt data method that set the size inside the vector to the size of the previously used vector without modifying the buffer.

here is a link to godbolt for the example.

https://godbolt.org/z/fcox5vTdE


Solution

  • What you want is not to adopt the memory resource - because you can and do - but to also store the vector itself there.

    You can, e.g. using a helper to create unique_ptr's with custom deleters:

    template <typename T>
    inline static auto make_T(Mem& mem) {
        std::pmr::polymorphic_allocator<T> alloc(&mem);
        return std::unique_ptr{
            alloc.template new_object<T>(), // allocator is propagated
            [alloc](T* p) { alloc.delete_object(p); }};
    }
    

    It requires a bit more work when the library support for C++20 is not complete, so let me show that in a live demo on Compiler Explorer

    However, there's no way you can "resurrect" a non-trivial type on the same memory using the C++ abstract machine model of memory.

    Live Objects In Buffers - Enter Boost Interprocess

    What you really want is to store /live objects/ there and access them from somewhere else.

    You can do this using shared-memory allocators, like from Boost Interprocess. You can do it on

    • manually allocated buffer (like the monotonous memory resource)
    • a managed shared memory segment
    • a managed mapped file

    Of course there's the benefit of persistence and multi-process access in the latter cases.

    If you don't need IPC or persistence, but like the managed segment abilities, use managed_external_buffer

    Demo Using Managed External Buffer

    It takes a few typedefs to set up:

    namespace Shared {
        using Mem = bip::managed_external_buffer;
    
        template <typename T>
        using Alloc = boost::container::scoped_allocator_adaptor<
            bip::allocator<T, Mem::segment_manager>>;
    
        template <typename T>
        using Vector = boost::container::vector<T, Alloc<T> >;
    
        template <typename K, typename V, typename Cmp = std::less<K>>
        using Map =
            boost::container::flat_map<K, V, std::less<K>, Alloc<std::pair<K, V>>>;
    } // namespace Shared
    

    Note how I used the scoped_allocator_adaptor to get as close to PMR's behaviour of propagating the container allocator to element types according to uses_allocator<>.

    struct MessageBody
    {
        using map_t = Shared::Map<char, char>;
        using vector_t = Shared::Vector<char>;
    
        template <typename Alloc>
        MessageBody(Alloc alloc) : vec_(alloc), map_(alloc)
        { }
    
        vector_t vec_;
        map_t    map_;
    };
    

    No more unique_ptr required, your data structure is basically what you had, but we store everything in a single memory resource, so the whole thing can be "resurrected" as one:

    int main() {
      std::array<char, 1000> buffer;
      buffer.fill('\0');
    
      {
          Shared::Mem mem(bip::create_only, buffer.data(), buffer.size());
    
          auto& message = *mem.find_or_construct<MessageBody>("message")(
              mem.get_segment_manager());
    
          message.vec_.push_back('1');
          assert(message.vec_.size() == 1);
      }
    
      {
          Shared::Mem mem(bip::open_only, buffer.data(), buffer.size());
    
          auto& message = *mem.find_or_construct<MessageBody>("message")(
              mem.get_segment_manager());
    
          assert(message.vec_.size() == 1);
      }
      std::cout << "Bye" << "\n";
    }
    

    The get_segment_manager() call returns a pointer that serves as the initializer for Shared::Alloc<> instances.

    Now it runs passes the assert: Live On Coliru

    Just prints

    Bye