user16538449
user16538449

Reputation:

How to make std::vector use already allocated memory for its internal array?

Is there a way to move already initialized data into a std::vector?

Here's my own simple vec class:

template<typename T>
class vec final
{
    std::unique_ptr<T[]> pValues;
    size_t size = 0;
public:
    std::span<T> span() const { return std::span<int>(pValues.get(), size); }
    vec(T* p, size_t sz) : size(sz) { pValues.reset(p); }
};

As you can see, it will take ownership of the memory passed to it:

int main()
{
    constexpr size_t count = 99;
    auto orig = std::make_unique<int[]>(count);
    {
        std::span<int> orig_(orig.get(), count);
        std::iota(orig_.begin(), orig_.end(), -1);
        assert((orig_[0] == -1) && (orig_[1] == 0) && (orig_[98] == 97));
    }

    vec<int> nums(orig.release(), count);
    auto nums_ = nums.span();
    assert((nums_[0] == -1) && (nums_[1] == 0) && (nums_[98] == 97));
}

This all works "as desired," but I'd like to do something similar with std::vector; in particular, I do not want to copy the data into a std::vector (imagine count being significantly larger than 99).

In other words, I'd like to do the "copy around some pointers" that (usually) happens when I std::move one std::vector to another; but the source is my own pointer. As my sample code shows, it's "easy" enough to do, but I don't want my own vec.

When I'm done, I'd like to "traffic" in a std::vector because that way I can completely forget about memory management (and having do further extend my own vec class). Using a std::vector also works well with existing C++ code that can't be changed.

Upvotes: 5

Views: 1301

Answers (1)

Luis Guzman
Luis Guzman

Reputation: 1016

You can have a std::vector use already allocated memory by providing it with a custom allocator:

#include <limits>
#include <iostream>
#include <memory>
#include <vector>
#include <numeric>
#include <cassert>

// allocator adapter to use pre-allocated memory
template <typename T, typename A=std::allocator<T>>
class reuse_mem_allocator : public A {
  typedef std::allocator_traits<A> a_t;

public:
  typedef typename a_t::size_type size_type;
  typedef typename a_t::pointer pointer;

  template <typename U> struct rebind {
    using other =
      reuse_mem_allocator<
        U, typename a_t::template rebind_alloc<U>
      >;
  };

 // have to store a ptr to pre-allocated memory and num of elements
 reuse_mem_allocator(T* p = nullptr, size_type n = 0) throw()
      : p_(p)
      , size_(n)
  { }

  reuse_mem_allocator(const reuse_mem_allocator& rhs) throw()
  : p_(rhs.p_)
  , size_(rhs.size_)
  { }

  // allocate but don't initialize num elements of type T
  pointer allocate (size_type num, const void* = 0) {
    // Unless, it is the first call, and
    // it was constructed with pre-allocated memory.
    if (size_ != 0) {
      if (num == size_) {
        // Then, don't allocate; return pre-allocated mem
        size_ = 0;  // but only once
        return p_;
      } else {
        throw std::bad_alloc();
      }
    } else {
      // allocate memory with global new
      T* ret = (T*)(::operator new(num*sizeof(T)));
      return ret;
    }
  }

  // convert value initialization into default/new initialization
  template <typename U>
  void construct(U* ptr)
    noexcept(std::is_nothrow_default_constructible<U>::value) {
    ::new(static_cast<void*>(ptr)) U;
  }
  
  template <typename U, typename...Args>
  void construct(U* ptr, Args&&... args) {
    a_t::construct(static_cast<A&>(*this),
                   ptr, std::forward<Args>(args)...);
  }
     
  private:
    pointer p_;
    size_type size_;
};


int main()
{
   constexpr size_t count = 9;
   auto orig = std::make_unique<int[]>(count);
   std::iota(orig.get(), orig.get()+count, -1);
   assert((orig[0] == -1) && (orig[1] == 0) && (orig[count-1] == count-2));
       
   std::vector<int, reuse_mem_allocator<int>> num(count, reuse_mem_allocator(orig.release(), count));
   for (auto e : num) {
       std::cout << e << " ";
   }
   std::cout << "\n";
   std::cout << "size: " << num.size() << "\n";
}

I compiled it with c++17. Here is the output:

-1 0 1 2 3 4 5 6 7 
size: 9

The allocator adapter is based on the one from this answer.

Upvotes: 3

Related Questions