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
?
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
).