1

I recently realized (pretty late in fact) that it's important to have move constructors marked as noexcept, so that std containers are allowed to avoid copying.

What puzzles me is why if I do an erase() on a std::vector<> the implementations I've checked (MSVC and GCC) will happily move every element back of one position (correct me if I'm wrong). Doesn't this violate the strong exception guarantee?

In the end, is the move assignment required to be noexcept for it to be used by std containers? And if not, why is this different from what happens in push_back?

Costantino Grana
  • 3,132
  • 1
  • 15
  • 35
  • 1
    Regarding ``erase()``, here https://en.cppreference.com/w/cpp/container/vector/erase it says "Does not throw unless an exception is thrown by the assignment operator of T.". This seems to imply that ``noexcept`` is not required for the assignment operator. But I might be mistaken. – Christian Halaszovich Nov 09 '22 at 12:51
  • 1
    It is mostly a performance thing, noexcept move constructors get handled in a more optimized way in STL containers because they know moving objects around in memory will not go wrong halfway the process (so the move can sometimes be done in bulk instead of one item at a time). – Pepijn Kramer Nov 09 '22 at 12:53
  • 1
    the standard seems to be very specific for vector: `§ 26.3.11.5 ... If an exception is thrown while inserting a single element at the end and T is CopyInsertable or is_nothrow_move_constructible_- v is true, there are no effects. Otherwise, if an exception is thrown by the move constructor of a non-CopyInsertable T, the effects are unspecified.` - "no effects" requirement is only when inserting single element at the end. it does not seem to be possible to have strong exception safety if it happens when reallocating/swapping elements in the container. – dewaffled Nov 09 '22 at 14:54

1 Answers1

1

Here I am only guessing at the rationale, but there is a reason for which push_back might benefit more from a noexcept guarantee than erase.

A main issue here is that push_back can cause the underlying array to be resized. When that happens, data has to be moved (or copied) between the old and the new array.

If we move between arrays, and we get an exception in the middle of the process, we are in a very bad place. Data is split between the two arrays with no guarantees to be able to move/copy and put it all together in a single array. Indeed, attempting further moves/copies could only raise more exceptions. Since we caa only keep either the old or the new array in the vector, one "half" of the data will simply be lost, which is tragic.

To avoid the issue, one possible strategy is to copy data between arrays instead of moving them. If an exception is raised, we can keep the old array and lose nothing.

We can also use an improved strategy when noexcept moves are guaranteed. In such case, we can safely move data from one array to the other.

By contrast, performing an erase does not resize the underlying array. Data is moved within the same array. If an exception is thrown in the middle of the process, the damage is much more contained. Say we are removing x3 from {x1,x2,x3,x4,x5,x6}, but we get an exception.

{x1,x2,x3,x4,x5,x6}
{x1,x2,x3 <-- x4,x5,x6}  move attempted
{x1,x2,x4,<moved>,x5,x6}  move succeeded
{x1,x2,x4,<moved> <-- x5,x6}  move attempt
{x1,x2,x4,<moved>,x5,x6}  move failed with an exception

(Above, I am assuming that if the move assignment fails with an exception, the object we are moving from is not affected.)

In this case, the result is an array with all the wanted objects. No information in the objects is lost, unlike what happened with resizing using two arrays. We do lose some information, since it might not be easy to spot the <moved> object, and distinguish the "real" data from the extraneous <moved>. However, even in that position, this information loss is much less tragic than losing half of the vector's objects has it would happen with a naive implementation of resizing.

Copying objects instead of moving them would not be that useful, here.

Finally, note that noexcept is still useful in the erase case, but is it not as crucial as it is when resizing a vector (e.g., push_back).

chi
  • 111,837
  • 3
  • 133
  • 218
  • Not sure if having a "little" mistake in the array is so easy to ignore, since one element is now in an invalid state, but still a good guess. – Costantino Grana Nov 09 '22 at 18:29
  • @CostantinoGrana I agree. It would be nice if, when the move throws an exception `e`, `erase` caught that and re-threw the pair `(e,position)` so that the caller would get enough information. Not an ideal workaround, though. – chi Nov 09 '22 at 18:54