Skip to content

Latest commit

 

History

History
107 lines (88 loc) · 4.23 KB

assume-move-operations-are-not-cheap-present-or-used.md

File metadata and controls

107 lines (88 loc) · 4.23 KB

Assume move operations are not present, cheap or used

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:

  1. When move semantics are not present.
  2. When move semantics are not any cheaper than copying.
  3. When move semantics are present, but not employed.

When Move Semantics Are Not Present

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:

  1. the copy-constructor or assignment-operator,
  2. any of the two move-operations or
  3. 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 defaulting 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.

When Move Semantics Are Not Cheap

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.

When Move Semantics Are Not Used

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.