Search code examples
c++noexceptmove-assignment-operator

When is `noexcept` required on move assignment?


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?


Solution

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