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(); }
noexcept is nice for two reasons: The compiler can optimize a little better because it doesn't need to emit any code for unwinding a call stack in case of an exception, and. It leads to incredible performance differences at runtime for std::vector (and other containers, too)
This is because the default implementations of these functions which are automatically added to your class if you don't replace them (much like the default destructors) are created with the correct noexcept properties to allow moving the objects.
Inheriting constructors and the implicitly-declared default constructors, copy constructors, move constructors, destructors, copy-assignment operators, move-assignment operators are all noexcept(true) by default, unless they are required to call a function that is noexcept(false) , in which case these functions are ...
A move constructor enables the resources owned by an rvalue object to be moved into an lvalue without copying. For more information about move semantics, see Rvalue Reference Declarator: &&. This topic builds upon the following C++ class, MemoryBlock , which manages a memory buffer.
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.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With