Monday, 5 November 2012

Doing C++11 Move Semantics Right in gcc 4.7

Since upgrading to gcc 4.7 a while ago I have been making use of some of the new C++11 features available. Specifically, I have found the introduction of r-value references, which enable move semantics and perfect forwarding, to be very useful. A very good explanation of both of these topics is given by Thomas Becker (http://thbecker.net/articles/rvalue_references/section_01.html). The purpose of this post is to instead explain a pitfall (bug?) that I stumbled into when implementing move semantics for a class in gcc 4.7,

The Problem

For a user-specified class Foo in C++11 you can specify a move constructor Foo(Foo &&) and a move assignment operator Foo & operator=(Foo &&) which enable move semantics. One of the primary purposes of move semantics is to enable the shifting of resources from an instance of an object to another. For example, when an instance is about to go out of scope move semantics allow you to specify how to efficiently "pass on" the underlying data without having to resort to deep copying. As an example, an Array class with a copy constructor and copy assignment operator has had move semantics added:

This all appears to be working properly as the move constructor and move assignment operators are called correctly when std::move is used to force an r-value reference. The next thing to try is using this with the std::vector container as the move semantics should now allow storage of the objects directly in the vector and all of the reshuffling can be handled efficiently with move semantics:

As shown, as each item is pushed onto the vector it is move-constructed into memory regardless of whether a resize was required or not. However, as the vector is resized copy constructors are being called to reconstruct the other elements into place. Why is the move constructor not being used?

The Cause

An interesting way to find the cause of this behaviour is to fire up gdb and step through to see how std::vector operates and to see what static decisions are made to determine whether a move or a copy is made. Tracing the push_back calls you land in _M_emplace_back_aux in vector.tcc:

When the size of the vector is insufficient, the allocator is called to set __new_start. The object which was pushed onto the vector is then directly move-constructed into memory as expected. Finally, a call to __uninitialized_move_if_noexcept_a is made which moves/copies the objects from their old location to the new section of memory.

Looking into stl_unitialized.h, we can see that __uninitialized_move_if_noexcept_a wraps a call to __unitialized_copy_a, which wraps the __first and __last iterators in the _GLIBCXX_MAKE_MOVE_IF_NOEXCEPT_ITERATOR macro. Inspecting stl_iterator.h shows this is just a call to std::__make_move_if_noexcept_iterator (which is also in stl_iterator.h):

_ReturnType is therefore set to _Iterator if the type it iterates over is notnothrowMoveConstructibleand is CopyConstructible, otherwise a special move iterator is used. Therefore, it seems we need Array to be nothrowMoveConstructible and consulting http://en.cppreference.com/w/cpp/types/is_move_constructible says that all is required is to specify the move constructors as noexcept.

The Solution

Making the move constructor and move assignment operator noexcept, however, is not enough and does not change the output. Digging further into typetraits it can be seen that Array fails __is_nt_constructible_impl under is_nothrow_constructible: