Generic Name
Generic Name

Reputation: 1270

Why does a throwing move constructor result in copying instead of moving where a strong exception guarantee is given?

Everywhere I look it seems to be the agreement that the standard library must call copy constructors instead of move constructors when the move constructor is noexcept(false).

Now I do not understand why this is the case. And furthermore, Visual Studio VC v140 and gcc v 4.9.2 seems to do this differently.

I do not understand why noexcept this is a concern of e.g. vector<T>. I mean, how should vector<T>::resize() be able to give strong exception guarantee if T does not? As I see it, the exception level of vector will be dependent on T, regardless if copy or move is used. I understand noexcept to solely be a wink to the compiler to do exception handling optimization.

This little program calls the copy constructor when compiled with gcc and move constructor when compiled with Visual Studio.

include <iostream>
#include <vector>

struct foo {
  foo() {}
  //    foo( const foo & ) noexcept { std::cout << "copy\n"; }
  //    foo( foo && ) noexcept { std::cout << "move\n"; }
  foo( const foo & )  { std::cout << "copy\n"; }
  foo( foo && )  { std::cout << "move\n"; }

  ~foo() noexcept {}
};

int main() {
    std::vector< foo > v;
    for ( int i = 0; i < 3; ++i ) v.emplace_back();
}

Upvotes: 37

Views: 19787

Answers (2)

Sebastian Redl
Sebastian Redl

Reputation: 71889

This is a multi-faceted question, so bear with me while I go through the various aspects.

The standard library expects all user types to always give the basic exception guarantee. This guarantee says that when an exception is thrown, the involved objects are still in a valid, if unknown, state, that no resources are leaked, that no fundamental language invariants are violated, and that no spooky action at a distance happened (that last one isn't part of the formal definition, but it is an implicit assumption actually made).

Consider a copy constructor for a class Foo:

Foo(const Foo& o);

If this constructor throws, the basic exception guarantee gives you the following knowledge:

  • No new object was created. If a constructor throws, the object is not created.
  • o was not modified. Its only involvement here is via a const reference, so it must not be modified. Other cases fall under the "spooky action at a distance" heading, or possibly "fundamental language invariant".
  • No resources were leaked, the program as a whole is still coherent.

In a move constructor:

Foo(Foo&& o);

the basic guarantee gives less assurance. o can be modified, because it is involved via a non-const reference, so it may be in any state.

Next, look at vector::resize. Its implementation will generally follow the same scheme:

void vector<T, A>::resize(std::size_t newSize) {
  if (newSize == size()) return;
  if (newSize < size()) makeSmaller(newSize);
  else if (newSize <= capacity()) makeBiggerSimple(newSize);
  else makeBiggerComplicated(newSize);
}
void vector<T, A>::makeBiggerComplicated(std::size_t newSize) {
  auto newMemory = allocateNewMemory(newSize);
  constructAdditionalElements(newMemory, size(), newSize);
  transferExistingElements(newMemory);
  replaceInternalBuffer(newMemory, newSize);
}

The key function here is transferExistingElements. If we only use copying, it has a simple guarantee: it cannot modify the source buffer. So if at any point an operation throws, we can just destroy the newly created objects (keep in mind that the standard library absolutely cannot work with throwing destructors), throw away the new buffer, and rethrow. The vector will look as if it had never been modified. This means we have the strong guarantee, even though the element's copy constructor only offers the weak guarantee.

But if we use moving instead, this doesn't work. Once one object is moved from, any subsequent exception means that the source buffer has changed. And because we have no guarantee that moving objects back doesn't throw too, we cannot even recover. Thus, in order to keep the strong guarantee, we have to demand that the move operation doesn't throw any exceptions. If we have that, we're fine. And that's why we have move_if_noexcept.

As to the difference between MSVC and GCC: MSVC only supports noexcept since version 14, and since that is still in development, I suspect the standard library hasn't been updated to take advantage yet.

Upvotes: 61

Puppy
Puppy

Reputation: 146900

The core issue is that it's impossible to offer strong exception safety with a throwing move constructor. Imagine if, in vector resize, half way through moving the elements to the new buffer, a move constructor throws. How could you possibly restore the previous state? You can't use the move constructor again because, well, that could just keep throwing.

Copying works for strong exception safety guarantee regardless of it's throwing nature because the original state is not damaged, so if you can't construct the whole new state, you can just clean up the partially-built state and then you're done, because the old state is still here waiting for you. Move constructors don't offer this safety net.

It's fundamentally impossible to offer strongly exception safe resize() with a throwing move, but easy with a throwing copy. This fundamental fact is reflected everywhere over the Standard library.

GCC and VS treat this differently because they are in different stages of conformance. VS has left noexcept to be one of the last features they implement, so their behaviour is a kind of mid-way between C++03's behaviour and C++11/14's. Particularly, since they don't have the ability to tell if your move constructor is actually noexcept or not, they basically just have to guess. From memory they simply assume that it is noexcept because throwing move constructors are not common and not being able to move would be a critical problem.

Upvotes: 24

Related Questions