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 futher more 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. I mean how should vector::resize() be able to give strong exception guarantee if T does not. As I see it the exception level of vector will be dependend 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();
}
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:
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".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.