C++11 brought along with it the fundamentally game-changing concept of move-semantics. But does this mean that any 'ol C++98 (or newer) code will run faster immediately, just because of C++11? The answer is naturally: no. We will review three situations where move-semantics may not improve your performance automagically:
- When move semantics are not present.
- When move semantics are not any cheaper than copying.
- When move semantics are present, but not employed.
This first section deals with the phenomenon that move operators (i.e. constructors and assigment operators) may not always be implicitly generated by the compiler. This was already discussed in the note on special member function generation, but basically, if either:
- the copy-constructor or assignment-operator,
- any of the two move-operations or
- the destructor
has already been explicitly defined by the creator of a class, (the undefined) move operations will not be generated by the compiler. Its rationale is that if any of the above operations require some special implementation, then the usually implicitly generated member functions do too, and should thus not be compiler-generated. Therefore, this legacy C++98 class will not actually be viable for moving:
class Foo {
public:
Foo(int ryan); // OK
Foo(const int& x); // Not OK, blocks move operations
private:
int ryan;
};
In this case, you would have to explicitly define them, even by default
ing
them. Note that generation of the destructor and copy-assignment-operator by the
compiler in the presence of another explicitly defined special member function
still works, but is deprecated. So this is the correct change to our now
C++11-move-semantics-aware legacy class:
class Foo {
public:
Foo(int ryan); // OK
Foo(const int& x); // Not OK, blocks move operations
Foo(int&& x) = default;
Foo& operator=(const int&) = default;
Foo& operator=(int&& x) = default;
~Foo() = default;
private:
int ryan;
};
Of course, at this point one should consider the copy-swap-idiom.
Next, it is also fallacious to think that all move-operations are cheap. As soon
as you start to think abou how the move operation might work, you can see that
for certain classes, moving will not be just magically cheap. First, let's see
where move semantics are cheap: standard containers that allocate on the heap,
such as std::vector
:
std::vector<int> v = {1, 2, 3};
auto w = std::move(v); // Will be cheaper than copying
In this case, the move operation need only to copy v
's pointer into that of
w
, and then set v
's pointer to nullptr
. But now let's consider the same
with std::array
:
std::array<int, 3> a = {1, 2, 3};
auto w = std::move(v); // Will be NOT cheaper than copying
std::array
is a thin wrapper around an array, basically encapsulating a
T[]
. Because you cannot move an array (only a pointer), i.e. set one to null,
you can only iterate over such an array's elements and move them
individually. This is still faster than copying on a per-element basis, but it
still requires O(N)
time (the constant may become less). The same goes for
std::string
's employing the small-string-optimiztion (SSO), where each string
contains a small fixed-size buffer to store short strings, observing that most
strings are small.
This pertains to the situation where a move may be possible, but is not
used. Most prominently, std::vector
and other standard containers will only
move their contents upon resizing (i.e. re-allocating and moving its contents)
if the contained object's move constructor is declared noexcept
. For this, it
uses the std::move_if_noexcept
function. The reason why is that it states the
strong-exception guarantee for its resizing-operations. This means that its
resizing is atomic, i.e. it either succeeds completely or fails
completely. More precisely, this means that it must ensure that it can move all
its contents to another place, without the risk of only moving one part and then
having one move throw an exception in the middle of resizing. Then both the new
and old memory would be only partially valid.