7

Suppose I have a class which manages a pointer to an internal buffer:

class Foo
{
  public:

  Foo();

  ...

  private:

  std::vector<unsigned char> m_buffer;
  unsigned char* m_pointer;
};

Foo::Foo()
{
  m_buffer.resize(100);
  m_pointer = &m_buffer[0];
}

Now, suppose I also have correctly implemented rule-of-3 stuff including a copy constructor which copies the internal buffer, and then reassigns the pointer to the new copy of the internal buffer:

Foo::Foo(const Foo& f) 
{
  m_buffer = f.m_buffer;
  m_pointer = &m_buffer[0];
}

If I also implement move semantics, is it safe to just copy the pointer and move the buffer?

Foo::Foo(Foo&& f) : m_buffer(std::move(f.m_buffer)), m_pointer(f.m_pointer)
{ }

In practice, I know this should work, because the std::vector move constructor is just moving the internal pointer - it's not actually reallocating anything so m_pointer still points to a valid address. However, I'm not sure if the standard guarantees this behavior. Does std::vector move semantics guarantee that no reallocation will occur, and thus all pointers/iterators to the vector are valid?

Channel72
  • 24,139
  • 32
  • 108
  • 180
  • I know that `swap` guarantees the pointers will still be valid, but I am not sure about moves. – R. Martinho Fernandes Jul 26 '13 at 12:06
  • 1
    There was a very lengthy discussion about these issues on a [related question](http://stackoverflow.com/q/17730689/1782465). I believe the conclusion is that in a move ctor, the buffer is guaranteed to be just transfered. In a move assignment op, it can happen (based on allocator properties) that the buffer will be copied. – Angew is no longer proud of SO Jul 26 '13 at 12:13
  • 3
    Shouldn't you be doing `f.m_pointer = nullptr;` in the body of the move constructor? Why have it point to an address that isn't its siblings? If you've a coupling between two variables that's pretty tight, I'd say keep it that way instead of making a cross coupling. – legends2k Jul 26 '13 at 14:16

4 Answers4

5

I'll not comment the OP's code. All I'm doing is aswering this question:

Does std::vector move semantics guarantee that no reallocation will occur, and thus all pointers/iterators to the vector are valid?

Yes for the move constructor. It has constant complexity (as specified by 23.2.1/4, table 96 and note B) and for this reason the implementation has no choice other than stealing the memory from the original vector (so no memory reallocation occurs) and emptying the original vector.

No for the move assignment operator. The standard requires only linear complexity (as specified in the same paragraph and table mentioned above) because sometimes a reallocation is required. However, in some cirsunstances, it might have constant complexity (and no reallocation is performed) but it depends on the allocator. (You can read the excelent exposition on moved vectors by Howard Hinnant here.)

Community
  • 1
  • 1
Cassio Neri
  • 19,583
  • 7
  • 46
  • 68
5

I'd do &m_buffer[0] again, simply so that you don't have to ask these questions. It's clearly not obviously intuitive, so don't do it. And, in doing so, you have nothing to lose whatsoever. Win-win.

Foo::Foo(Foo&& f)
   : m_buffer(std::move(f.m_buffer))
   , m_pointer(&m_buffer[0])
{}

I'm comfortable with it mostly because m_pointer is a view into the member m_buffer, rather than strictly a member in its own right.

Which does all sort of beg the question... why is it there? Can't you expose a member function to give you &m_buffer[0]?

Lightness Races in Orbit
  • 378,754
  • 76
  • 643
  • 1,055
  • Well, I suppose other member functions may advance the pointer or something. – R. Martinho Fernandes Jul 26 '13 at 13:33
  • 1
    Yeah, could be the case. If that's true, if this `m_pointer` is really a _cursor_ whose state is expected to change independently of the buffer itself, then this answer is wrong. But then I'd propose storing an index instead of a pointer. Maybe. And you can at least just copy the index, then. – Lightness Races in Orbit Jul 26 '13 at 13:33
2

A better way to do this may be:

class Foo
{

  std::vector<unsigned char> m_buffer;
  size_t m_index;

  unsigned char* get_pointer() { return &m_buffer[m_index];
};

ie rather than store a pointer to a vector element, store the index of it. That way it will be immune to copying/resizing of the vectors backing store.

Andrew Tomazos
  • 66,139
  • 40
  • 186
  • 319
  • That's a good answer, but it doesn't really answer what I asked. I'm aware that using indexes is safer, but I'd like to know if the move semantics of `std::vector` invalidate iterators/pointers? – Channel72 Jul 26 '13 at 12:05
1

The case of move construction is guaranteed to move the buffer from one container to the other, so from the point of view of the newly created object, the operation is fine.

On the other hand, you should be careful with this kind of code, as the donor object is left with a empty vector and a pointer referring to the vector in a different object. This means that after being moved from your object is in a fragile state that might cause issues if anyone accesses the interface and even more importantly if the destructor tries to use the pointer.

While in general there won't be any use of your object after being moved from (the assumption being that to be bound by an rvalue-reference it must be an rvalue), the fact is that you can move out of an lvalue by casting or by using std::move (which is basically a cast), in which case code might actually attempt to use your object.

David Rodríguez - dribeas
  • 204,818
  • 23
  • 294
  • 489
  • 1
    A solution to the fragility is to zero the rvalue source's `m_pointer`, but I like to use a type to do that for me, e.g. [`tidy_ptr`](https://gitorious.org/redistd/redistd/blobs/master/include/redi/tidy_ptr.h), which then allows `Foo(Foo&&) = default;` to do the right thing. – Jonathan Wakely Jul 26 '13 at 14:32
  • @JonathanWakely: I did not want to dig into the details, as I don't particularly know how the component is used. The important bit is that the destructor must be able to handle the case of an empty vector and either ignore the pointer or have it set to 0 and then ignore it. – David Rodríguez - dribeas Jul 26 '13 at 17:26