Saturday, July 4, 2015

Move semantics with containers and algorithms

Move semantics is one of the coolest new features of (the-now-so-not-that-new) C++11. The main idea is the ability to steal the implementation (the guts) of temporary objects and therefore be able to avoid creating object copies. This of course is very important for performance; while it is true that many mainstream C++ compilers have been able to optimize away such copies (e.g. return value elimination), move-semantics gives us developers an explicit way of controlling it.

Another important aspect of move-semantics is that it allows definition of move-only data types, a.k.a. objects that cannot be copied but only moved. An example for all, move-semantics makes possible the definition of the std::unique_ptr<T> data type.

Let us consider a simple ADT, called Object, with properties very similar to the ones of an std::unique_ptr. As you can see it is a simply wrapper for a string literal allocated on the heap that only offers move semantics (no copying is allowed). It additionally contains a type that will be useful later.
We can use it populate a container as shown in the main function. emplace_back allows for the object to be built in place so no copy constructor is going to be invoked.

Real problems start when we want to use this container with standard algorithms. If the algorithm uses move-semantics then there are no issues, for example trying to sort the array can be achieved with the following code:
If we print the addresses at which each message is allocated we see that it is indeed the same before and after sorting. This happens because std::sort internally uses std::swap which uses move-semantics.
// before sorting
obj(1st message) addr(str): 0x12a9030
obj(2nd message) addr(str): 0x12a9080
obj(3rd message) addr(str): 0x12a9010
obj(4th message) addr(str): 0x12a90f0
// after sorting
obj(1st message) addr(str): 0x12a9030
obj(4th message) addr(str): 0x12a90f0
obj(2nd message) addr(str): 0x12a9080
obj(3rd message) addr(str): 0x12a9010

So far so good! However, when trying to use algorithms which require copy, i.e. std::copy, std::copy_if & friends, the compiler is going to give us a nasty error message (which may change depending on your compiler, I am using GCC 4.9.2):

In instantiation of ‘_OIter std::copy_if(_IIter, _IIter, _OIter, _Predicate) [with _IIter = __gnu_cxx::__normal_iterator >; _OIter = __gnu_cxx::__normal_iterator >; _Predicate = main(int, char**)::]’:
container_move.cpp:67:63:   required from here
/usr/include/c++/4.9/bits/stl_algo.h:748:16: error: use of deleted function ‘Object& Object::operator=(const Object&)’
      *__result = *__first;
                ^
container_move.cpp:24:10: note: declared here
  Object& operator=(const Object&) = delete;
          ^

Which basically means the algorithm is trying to copy an instance of the Object class, but that is not allowed since we deleted the copy constructor. How to solve it? Well, you could write your implementation of std::copy_if and explicitly use std::move to promote the operation to move semantics, e.g.:
However there is an easier and cleaner way of achieving the same behaviour, behold the std::move_iterator. It is an adaptor for iterators which yields an R-value reference when the dereference operator (i.e. *) is used. That means using std::copy_if on a container with only moveable objects is as easy as:
Printing the dst_objs array yields the following:

obj(1st message) addr(str): 0x12a9030
obj(4th message) addr(str): 0x12a90f0

No object have been copied, mission accomplished! :)

It has to be added that

C++ <3