Move Construction, part IV


In my previous post, we discussed std::move(), a new function which is actually just a cast and has no performance overhead. It’s good to recall the problem we’re trying to solve.

v.push_back( Texture(...) ); // copy from a temporary

In the scenario above, we create a temporary texture object, then we create another texture object in the v container, copy the temporary to the container, then tear down the temporary Texture. If the Texture object is expensive to create or copy or destroy, then the code above generates performance overhead we’d like to avoid.

With move constructors in C++11, we can make the code above run supremely fast by swapping the guts of the temporary object directly into the vector, completely avoiding copying bits. The move constructor for Texture looks like this:

Texture( Texture&& rhs ) : // move ctor
   mpBits( std::move( rhs.mpBits ) ),
   mSize ( std::move( rhs.mSize  ) )
{
   rhs.mpBits = nullptr;
}

It’s clear from this code that we have a move constructor. There are two key elements that tip off the reader: the && rvalue reference notation and the std::move() function.

It’s important that all move constructors take a non-const rvalue reference. The reason is because we’re actually changing the internals of rhs. We’re extracting the contents — in this case mpBits. We need to set mpBits to nullptr so that when the destructor for the rhs temporary object is run, we won’t actually free the memory, since we’ve transferred ownership of that memory to a new object.

Secondly, recall that std::move() is simply a cast to an rvalue reference. Assuming mSize is an integer, the line

mSize ( std::move( rhs.mSize  ) )

is equivalent to:

mSize( rhs.mSize )

So why use std::move() at all? Two reasons:

  • std::move() tells the reader the intent. We are moving, not copying. In the case of PODs, moving and copying are equivalent, but that’s an implementation detail. Move constructors are new syntax, and one of the best ways to highlight that syntax is with std::move() notation.
  • std::move() continues to work properly should the element being moved from change to a non-POD type at some point in the future. If mSize is changed to a LargeNumber object, std::move() will ensure that the LargeNumber move constructor is called. If std::move() was not in place, we’d end up calling the LargeNumber copy constructor, which could be slower.

One of the delights of move constructors and std::move() is that you can introduce them into your code base gradually. If you call std::move() on an object that doesn’t have a move constructor, that’s OK. Due to the reference binding rules in C++11, you will simply end up calling the copy constructor. You may not get the best performance, but your code will still be correct. In the future, when you (or somebody else) adds a move constructor, your code will just run faster.

Similarly, you can add move constructors to your objects today. If the calling code doesn’t yet implement std::move() (in std::vector, say), that’s OK. The correct thing will still happen, and when you upgrade to code that enables std::move(), you’ll automatically get better performance.

Finally, some tips that summarize everything we’ve learned about move constructors:

  • Upgrade to C++11 compilers and libraries for free performance, since all standard containers, algorithms and objects are move enabled.
  • Ensure move constructors always take a non-const rvalue reference.
  • std::move is required because named objects are always lvalues.
  • Use std::move to explicitly eviscerate objects in the move constructor, even if those objects are (currently) PODs.
  • Step through new move constructors in your debugger to validate that move constructors are actually being called when you expect. Getting move constructors right can be tricky when you’re first starting out.
  • You can begin implementing move constructors in your own objects starting in low-level libraries or in high-level code — your choice. Implementation can be gradual.
  • If you implement a move constructor for an object, make sure you also implement move assignment.
  • The moved-from object will still have its destructor called. Ensure it is left in a safe state.
  • Returning expensive objects by value is more reasonable from a performance standpoint than in the past, because any temporary results can be moved from.
  • Avoid returning const T objects. Const objects cannot be moved from. Move constructors always take non-const objects, since the guts of the object must be changed.
  • By convention, ensure move constructors are as fast or faster than copy constructors. Avoid move constructors that are slower than copy constructors.

Enjoy moving!

Advertisements
This entry was posted in C++. Bookmark the permalink.

3 Responses to Move Construction, part IV

  1. Dukales says:

    You should check the http://www.cplusplus.com/reference/vector/vector/push_back/ . Second version of std::vector::push_back disprove your first assertion.

    • pkisensee says:

      tomilovanatoliy: If the Texture object has no move constructors, then neither std::vector::push_back( Texture&& ) nor std::vector::push_back( const Texture& ) can move the Texture object. It must be copied. Just because you may have a function that receives T&& does not mean the object will be moved. A T&& is a “universal reference” that can refer to either an lvalue or an rvalue. I haven’t talked in my blog yet about universal references or std::forward.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s