2

Is it mandatory, that the capacity of the std::vector is zero, after moving data from it? Assume that the memory allocators of source and destination vectors are always matching.

std::vector< int > v{1, 2, 3};
assert(0 < v.capacity());
std::vector< int > w;
w = std::move(v);
assert(0 == v.capacity());

Here said that move assignment operator leaves stealed RHS vector in a valid, but unspecified state. But nowhere pointed, that vector should perform additional memory allocations during move assignment operation. Another note is: vector have continuous memory region as underlying storage.

Tomilov Anatoliy
  • 15,657
  • 10
  • 64
  • 169

2 Answers2

2

If your question had been about move construction of a vector, the answer would be easy, the source vector is left empty after the move. This is because of the requirement in

Table 99 — Allocator-aware container requirements

Expression:

  X(rv)
  X u(rv)

Requires: move construction of A shall not exit via an exception.

post: u shall have the same elements as rv had before this construction; the value of u.get_allocator() shall be the same as the value of rv.get_allocator() before this construction.

Complexity: constant

(the A in the requirements clause is the allocator type)

The constant complexity leaves no option but to steal resources from the source vector, which means for it to be in a valid, but unspecified state you'd need to leave it empty, and capacity() will equal zero.


The answer is considerably more complicated in case of a move assignment. The same Table 99 lists the requirement for move assignment as

Expression:

  a = rv

Return type:

  X&

Requires: If allocator_traits<allocator_type>::propagate_on_container_move_assignment::value is false, T is MoveInsertable into X and MoveAssignable. All existing elements of a are either move assigned to or destroyed.

post: a shall be equal to the value that rv had before this assignment.

Complexity: linear

There are different cases to evaluate here.


First, say allocator_traits<allocator_type>::propagate_on_container_move_assignment::value == true, then the allocator can also be move assigned. This is mentioned in §23.2.1/8

... The allocator may be replaced only via assignment or swap(). Allocator replacement is performed by copy assignment, move assignment, or swapping of the allocator only if allocator_traits<allocator_type>::propagate_on_container_copy_assignment::value, allocator_traits<allocator_type>::propagate_on_container_move_assignment::value, or allocator_traits<allocator_type>::propagate_on_container_swap::value is true within the implementation of the corresponding container operation.

So the destination vector will destroy its elements, the allocator from the source is moved and the destination vector takes ownership of the memory buffer from the source. This will leave the source vector empty, and capacity() will equal zero.


Now let's consider the case where allocator_traits<allocator_type>::propagate_on_container_move_assignment::value == false. This means the allocator from the source cannot be move assigned to the destination vector. So you need to check the two allocators for equality before determining what to do.

If dest.get_allocator() == src.get_allocator(), then the destination vector is free to take ownership of the memory buffer from the source because it can use its own allocator to deallocate the storage.

Table 28 — Allocator requirements

Expression:

  a1 == a2

Return type:

  bool

returns true only if storage allocated from each can be deallocated via the other. ...

The sequence of operations performed is the same as the first case, except the source allocator is not move assigned. This will leave the source vector empty, and capacity() will equal zero.


In the last case, if allocator_traits<allocator_type>::propagate_on_container_move_assignment::value == false and dest.get_allocator() != src.get_allocator(), then the source allocator cannot be moved, and the destination allocator is unable to deallocate the storage allocated by the source allocator, so it cannot steal the memory buffer from source.

Each element from the source vector must be either move inserted or move assigned to the destination vector. Which operation gets done depends on the existing size and capacity of the destination vector.

The source vector retains ownership of its memory buffer after the move assignment, and it is up to the implementation to decide whether to deallocate the buffer or not, and the vector will most likely have capacity() greater than 0.


To ensure you do not run into undefined behavior when trying to resuse a vector that has been move assigned from, you should first call the clear() member function. This can be safely done since vector::clear has no pre-conditions, and will return the vector to a valid and specified state.

Also, vector::capacity has no pre-conditions either, so you can always query the capacity() of a moved from vector.

Praetorian
  • 106,671
  • 19
  • 240
  • 328
1

The state of the moved-from vector is unspecified but valid after the move, as you found out.

This means that it could really be in any valid state, in particular you can't assume that its capacity will be 0. It will probably be zero, and that would make a lot of sense, but that's not at all guaranteed.

But again, in practice if you don't care all that much about the standard, I suppose that you could rely on the capacity being 0. Due to the constraints on move operations, the vector move constructor pretty much has to steal memory from the moved-from one, leaving it empty. That's what will happen in almost all cases/implementations, but that's just not required.

A particularly twisted implementation could decide to reserve some elements in the moved-from vector just to mess with you. That technically wouldn't break any requirement.

tux3
  • 7,171
  • 6
  • 39
  • 51
  • Implementation of certain container may act on the way of swapping storages after destruction of the elements from left hand side container during move-assignment operation, isn't it? – Tomilov Anatoliy Jan 07 '15 at 23:21
  • I'm not sure I understand. Are you trying to say that an implementation could put the elements of the moved-to container in the moved-from container ? – tux3 Jan 07 '15 at 23:23
  • Firstly elements from LHS container destructed, if exists. Then simply swapping of memory storages (and size, capacity and other members of `std::vector` class). – Tomilov Anatoliy Jan 07 '15 at 23:25
  • Yes, that's possible. Although that wouldn't be such a good idea. Why wait instead of just freeing the unused memory now ? In practice the capacity does get zeroed like the question said : http://ideone.com/t5p4oq – tux3 Jan 07 '15 at 23:29