4

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.

  • 4
    `std::vector` doesn't provide access to its underlying data pointer for such thing :-/ – Jarod42 Sep 14 '21 at 13:53
  • You can move elements from one vector to another, but that involves N calls to the move constructor, and for "basic" types, moving is a copy. Vector owns it's buffer, so it can just merger two of those together. – NathanOliver Sep 14 '21 at 13:53
  • 1
    Not really, `std::vector` uses an allocator, allowing to 'adopt' data could be dangerous if such allocator uses arena allocations or such. – Kaldrr Sep 14 '21 at 13:55
  • 2
    You don't really have an option not to copy, if `vector` is what you want. `vector` requires contiguous storage, so it's going to have to move/copy the elements from the source vector into the destination vector, and possibly grow the buffer if that is required. – NathanOliver Sep 14 '21 at 13:56
  • 4
    If you don't want to copy the data in this case then you have to start with a vector in the first place. Then you can move it into another vector, which will just copy around some pointers. – Kevin Sep 14 '21 at 13:56
  • 4
    @JayBach you may consider using `std::span` instead of `std::vector` if that fits your needs – Nolan Sep 14 '21 at 14:11
  • 3
    @JayBach a `std::byte` isn't an `int`, so it doesn't make sense to move data from one vector to the other. You'd be breaking aliasing rules if you tried. – Kevin Sep 14 '21 at 14:14
  • 2
    "use already allocated memory for its internal array" should be possible when replacing the std::allocator, but I have never done this. So maybe google helps. Using an already filled array might not be possible. what if std::vector initializes the background memory? Guess you should try to replace the allocator, then resize the vector to the needed length and then fill the Data to the then shared memory. Sournds weired, but might work. – schnedan Sep 14 '21 at 20:10
  • And then someone constructs such vector by passing a pointer to an array on the stack ;-) Can't you create a vector of appropriate size first and then pass the pointer to its data wherever a pointer is needed? But also see @Kevin 's comment. Such casting between types is asking for trouble. – danadam Sep 14 '21 at 21:26
  • Oh, that's even worse. How would such vector know that on destruction it should call `free()` instead of `delete`? – danadam Sep 14 '21 at 21:32
  • 1
    I think it can be done by define an `allocator` using placement `new` inside, and pass it to the template parameter of `vector`. – wpzdm Sep 15 '21 at 14:02
  • The *correct* way would be to provide an apropriate allocator class. But that is tricky business that changed significantlly over the last couple of iterations of the C++ standard, and a bit beyond a simple SO answer, especially as this sounds like an XY problem. – DevSolar Sep 15 '21 at 18:56
  • 1
    The array versions of `std::unique_ptr` have `release()` and `reset()` functions just like the scalar versions. And you almost surely don't want a "real" `std::vector` when binding to an existing buffer, because the real one has lots of operations that need to reallocate. Rather you need a `std::unique_ptr` + `size()` + iterators, too bad there isn't one. – Ben Voigt Sep 15 '21 at 21:26

1 Answers1

2

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.

Luis Guzman
  • 996
  • 5
  • 8
  • @JayBach, what do you mean by compatible? Other than moving the entire vector... – Luis Guzman Sep 15 '21 at 17:06
  • 1
    @JayBach, you're right, but you could make `f(...)` a template: `template f(const T&)`. I'll keep thinking about it though... – Luis Guzman Sep 15 '21 at 17:31
  • 1
    @JayBach, I edited my answer. I added a way to move the `std::vector>` to a `std::vector`. I hope this helps. – Luis Guzman Sep 15 '21 at 18:54
  • 1
    The first part of this answer (do it with an allocator) is valid and a good answer to the question. The latter part (casting a portion of a `vector` to a `vector`) is a horrible implementation-specific hack that causes UB and should never see the light of day. – Ben Voigt Sep 15 '21 at 21:20
  • @BenVoigt, you're right that the edit, the later part, is implementation specific, and it only works if the allocator is placed first in the memory layout. I'll add a disclaimer to make it clear that it is implementation specific. I'll also add an assert to verify the memory layout. Or maybe, I'll just remove the edit. – Luis Guzman Sep 15 '21 at 22:11