Semantics of resize without element preservation using move idiom

121 views Asked by At

In std::vector there is a member function called resize. The utility of resize is two fold, first, it preserves elements the existing elements when it makes sense, and, second, it avoids allocations if they are not necessary. (in contrast to simply doing v = std::vector<T>(new_size); which would do neither.)

There is only one version of resize, one that takes the object by l-value reference (v.resize(new_size)). https://en.cppreference.com/w/cpp/container/vector/resize

Supposed I am writing a container similar to std::vector and I want to implement a version of resize that is r-value aware. I would say that, since the original object is in a "moved" state, element preservation is not necessary. Which in principle can produce a more efficient resize because element preservation is not necessary (this is particularly interesting in the case that the resize surpasses the current capacity.) I think the operation is valid

But I am not sure what is a consistent final state.

my_vector<double> v(3, 42.);

std::move(v).resize(6);

What should be the state of v? What static is more consistent with the current philosophy of moved objects?

Here some options:

  1. v has 6 elements and each element is totally unspecified.
  2. v is {42., 42., 42., 0., 0., 0.}
  3. v is {unspecified, unspecified, unspecified, 0., 0., 0.}
  4. v is {0., 0., 0., 0., 0., 0.}

Case 2 is what the current non r-value-aware vector::resize would do. I have a slight inclination towards case 3.

Implementation wise it is possible that 4 is the "likely" (i.e. undocumented) result (as an instance of the unspecified behavior of case 3). Since case 2 could also be a consistent instance, I seem that case 3 or 1 are the only formal options left.

Take into account that it is possible that resize to a larger value (6) requires reallocation.

I am using double for illustration as a place holder for a non-trivial type that might be expensive to copy. And 0. is not special except for being the default constructed state (T()). The question is equally valid with this slightly different case:

std::move(v).resize(6, 99.);

1. `v` has 6 elements and each element is totally unspecified.
2. `v` is `{42., 42., 42., 99., 99., 99.}`
3. `v` is `{unspecified, unspecified, unspecified, 99., 99., 99.}`
4. `v` is `{99., 99., 99., 99., 99., 99.}`

(Note that in any case std::move(v).resize(6); has the chance to do at least slight better than v = my_vector<T>(6) because the latter always allocate and always initializes all the elements.)

2

There are 2 answers

1
BoP On

A flaw in the reasoning is that std::move(x) in itself doesn't move anything. It could have been named enable_move or allow_move, if those names hadn't been too long. It marks things as "movable", but doesn't perform the move operation. So, as long as resize doesn't have an rvalue overload, nothing is moved by using std::move.

However, std::vector is already rvalue aware and will (likely) use the helper function move_if_noexcept() to move objects that have a noexcept move constructor.

This is used, for example, by std::vector::resize, which may have to allocate new storage and then move or copy elements from old storage to new storage. If an exception occurs during this operation, std::vector::resize undoes everything it did to this point, which is only possible if std::move_if_noexcept was used to decide whether to use move construction or copy construction. (unless copy constructor is not available, in which case move constructor is used either way and the strong exception guarantee may be waived).

6
Goswin von Brederlow On

There is a misconception of what std::move means and does. It does not move anything.

What std::move does is tell the compiler I don't need this anymore.

So when you say: std::move(v).resize(6); It's like you say you don't care about the result of resize(). (It's not what the compiler will do, but think of it that way). Can you see why this makes no sense?

So after the std::move call you have an object that can be moved. But instead of then moving it you call the resize() function. The resize() function never moves this, it remains right where it is. It will only change the internal state of the object, mainly the size of the heap allocated data. So the std::move has 0 effect at all.

What you really seem to care about isn't the vector but the objects the vector holds. When you call resize() there are 3 cases to consider:

  1. resizing to fewer elements than the vector holds

Here the extra elements need to have their destructor called. Nothing is moved or reallocated. The capacity of the vector doesn't change.

  1. growing the vector but within the capacity

Here the new elements are initialized. Nothing is moved or reallcoated.

  1. growing the vector beyond it's capacity

Here the vector has to allocate new memory. And then it has to copy or move elements to the new storage. Additionally the new elements are initialized.

Now here you do have a choice you can make. You say moving elements is expensive. The choice you can make is to clear() the vector before the resize(). That calls the destructor for all elements and then the resize will initialize all new elements for the full size. Ask yourself if destruction + construction is more expensive than move.

In all cases the final vector will be fully initialized no matter what you do. Maybe you were thinking of reserve() instead of resize()? You can call clear() before reserve too, which will call the destructor for existing elements and leave the whole vector uninitialized after reserve() with a size of 0.