Move Semantics Explained
Tags: cpp
Published: 2019-09-14 06:43:38


In `C++11` move semantics and rvalue references were added to the language. With their addition, we can significantly increase the performance by avoiding unnecessary copies of temporary objects.

## Lvalue and rvalue references

The first thing is to understand the concept of _lvalues_ and _rvalues_. Simply speaking, _lvalue_ expression may appear on the right or the left hand side of an assignment, whereas an _rvalue_ may appear only on the right hand side. This is an over simplified explanation, and in modern C++ it is more nuanced than that, but for our purposes we can look at some examples to make that clearer:

```cpp
int* int_ptr = nullptr;
// Example 1.
int i = 0;
int_ptr = &i;
/* Ok! because i is an lvalue,
for which the address can be referenced.*/
*int_ptr = 5;

// Example 2.
int func();
/* ERROR! that's an rvalue. We can't reference
the address of the temporary created on that call. */
func() = 5;

// Example 3.
int& func();
func() = 7;// OK! Like operator[] in std::vector for example.

//Example 4.
int a = 3, b = 4; // both lvalues
(a+b) = b; //ERROR
```

As we can see from the above examples, an _rvalue_ is an expression whose lifetime ends at the expression it is evaluated in. It can be thought of as an unnamed temporary object in that context.

## Move semantics

Suppose we have a class owning a resource which is expensive to construct or copy, for example a large buffer of memory.

```cpp
class Expensive {
  std::string name_;
  size_t big_arr_size_ = 0;
  uint8_t *big_array_ = nullptr;

 public:
  Expensive(const std::string &name, size_t arr_size)
      : name_(name),
        big_arr_size_(arr_size),
        big_array_(big_arr_size_ ? new uint8_t[arr_size] : nullptr) {
  }

  ~Expensive() {
    delete[] big_array_;
  }
};
```

The copy constructor and copy assignment operators:

```cpp
  Expensive(const Expensive &other)
      : name_(other.name_),
        big_arr_size_(other.big_arr_size_),
        big_array_(big_arr_size_ ? new uint8_t[big_arr_size_] : nullptr) {
    std::copy(other.big_array_, other.big_array_ + big_arr_size_, big_array_);
  }

  // note: Might be a better option, using the copy and swap idiom.
  Expensive &operator=(const Expensive &other) {
    if (&other != this) {
      name_ = other.name_;
      if (other.big_arr_size_ != big_arr_size_) {
        // In case throws, would not want to change the state of the object.
        auto new_size = other.big_arr_size_;
        auto new_arr = new_size ? new uint8_t[new_size] : nullptr;
        // resource needs clean up and reallocation
        delete[] big_array_;
        big_arr_size_ = new_size;
        big_array_ = new_arr;
      }
      std::copy(other.big_array_, other.big_array_ + big_arr_size_, big_array_);
      
    }
    return *this;
  }
```

During copy construction or copy assignment we allocate and assign values to a (potentially large) buffer for the constructed/assigned object.
Let’s look at the following example:

```cpp
Expensive first("first", 1024);
Expensive second("second", 2048);

// Assignment of two lvalue. Copy assignment is called.
first = second

/* Assignment of a temporary object,
which is destructed right after expression evaluation.
Copy assignment is called (no move operations defined yet),
but we can do better. */
first = Expensive("third", 4096);
```

Currently, the sequence of operations in the second assignment is just like in the first. After creating the temporary object, the assignment operator gets called, which in turn calls for allocation, deletion, and copy of the buffer. It could be more efficient to have a special treatment for the second case, where we are dealing with an _rvalue_,  a temporary object which will be destructed right after the assignment. In this way, we could replace the expensive assignment to something more similar to pointer swap, this way we won’t need to reallocate the data and copy its content. That’s where move semantics comes into play with the use of _rvalue_ reference. Syntactically, given a type `T`, `T&` is called _lvalue_ reference and `T&&` is _rvalue_ reference. When we want to construct or assign from _rvalue_ reference we call that a move operation. There is the move assignment and move construction operations, which in our example might look like this:

```cpp
  Expensive(Expensive &&other) {
    std::swap(name_, other.name_);
    std::swap(big_arr_size_, other.big_arr_size_);
    std::swap(big_array_, other.big_array_);
  }
```

In this implementation we swap the pointers of the buffer, a much cheaper operation than reallocation and copy. It’s important to leave the object that we moved from (called other in our example) in a well defined state, having its resources cleared. (The created object has empty string, null pointer and zero size, so that requirement is fulfilled).
Note that although “other” is passed as an _rvalue_ reference, it is an lvalue by itself, because it has an address we can reference. If the variable has a name, its an _lvalue_. The function `std::move` casts its argument to an _rvalue_ reference (doesn’t distinguish if its argument is an _lvalue_ or an _rvalue_) so it returns an _rvalue_.

 Let’s look at move assignment:

```cpp
  Expensive &operator=(Expensive &&other) {
    name_ = std::move(other.name_);
    std::swap(big_arr_size_, other.big_arr_size_);
    std::swap(big_array_, other.big_array_);
    delete[] other.big_array_;
    other.big_array_ = nullptr;
    other.big_arr_size_ = 0;
    return *this;
  }
```

Note the use of `std::move` on the `name_` member. Since `other.name` is an _lvalue_, if we had written `name_ = other.name_` then the copy c’tor of `std::string` would have been called. Since we want to leverage that its a move operation, in order to call the move assignment of `std::string` we could have just called `std::move` on `other.name`, which is returned as an rvalue. The implementation of `std::swap` is using `std::move` on its arguments when assigning, so we could have just called `std::swap` on `name_`, as it was written in the move c’tor.

## Compiler generated special member functions

The special member functions before C++11 were:  default c’tor, d’tor, copy c’tor and copy assignment operators. Those member functions were generated if needed ( i.e, they were called in the code) and not explicitly declared in the class. With move semantics, the compiler might generate two more member functions: the move c’tor and the move assignment operator. The move c’tor move constructs each non static data member of the class and similarly, move assignment move-assigns each non-static data member. If a data member is of a type that doesn’t ofer move operation then it will be copy constructed/assigned.

When the class explicitly defines a copy c’tor then the move operations won’t be generated, and if a move operation was explicitly defined, the copy operations won’t be generated. The logic behind that is that if memberwise copy is not appropriate for the class, then probably memberwise move is not either, and that goes the other way around too.
 The rule of three states that if you declare any of copy c’tor, copy assignment operator or destructor, you should declare all three. That rule came from the observation that if the class defines some special treatment in d’tor or copy operation, it probably makes some resource management and that resource should be addressed in the copy operations and in the destructor. This rule is the motivation behind the fact that if a d’tor was explicitly defined, the move operations won’t be generated. So compiler generates move operations only if they were not defined and no d’tor or copy operations were defined either.
 Now lets take a look at the implication of that last paragraph. Suppose we have a class that contains `std::vector`, for which we know the move operations are defined.

```cpp
struct ClassGenMove {
  ClassGenMove() : bytes_(10'000'000) {}
  std::vector<uint8_t> bytes_;
};
```

So in this example the class does not define d’tor or copy operations, which means the compiler will generate the copy operations, the move operations and the destructor. Since the compiler generated move operation make a memberwise move, move assignment or move construction will take place on `bytes_` member, when assigning or constructing from an _rvalue_.
What happens when we add a destructor:

```cpp
struct ClassNotGenMove {
  ClassNotGenMove() : bytes_(10'000'000) {}
  std::vector<uint8_t> bytes_;
  ~ClassNotGenMove() = default;
};
```

As previously noted, the compiler will not generate the move operations. And now we can test the performance for such a change.

```cpp
template <typename T>
void TimeMoveVsCopy() {
  T sample1, sample2;

  auto num_iter(10000);
  auto start_time = std::chrono::high_resolution_clock::now();
  for (auto i(0); i < num_iter; i++) {
    sample1 = std::move(sample2);
  }
  auto end_time = std::chrono::high_resolution_clock::now();

  std::cout << "Took "
            << std::chrono::duration_cast<std::chrono::microseconds>(end_time -
                                                                     start_time)
                       .count() /
                   num_iter
            << " us" << std::endl;
}
std::cout << "Class with generate move ops:\n";
TimeMoveVsCopy<ClassGenMove>();

std::cout << "Class without generated move ops:\n";
TimeMoveVsCopy<ClassNotGenMove>();
```

The performance as measured on a MacBook Pro (3.5 GHz Intel Core i7):

```sh
Class with generate move ops:
Took 6 us
Class without generated move ops:
Took 745 us
```

As expected, move operations were not generated and so the copy c’tor of `std::vector` was called instead, which we see as the impact on performance. Following the rules discussed earlier regarding the compiler generated special member function, after adding the destructor, we need to explicitly declare the move and copy operations, which in this case  it’s enough to just use the default. Now we are back to using the move operations as expected.

```cpp
ClassNotGenMove &operator=(const ClassNotGenMove &) = default;
ClassNotGenMove(const ClassNotGenMove &) = default;
ClassNotGenMove(ClassNotGenMove &&) = default;
ClassNotGenMove &operator=(ClassNotGenMove &&) = default;
```

### References

* <http://thbecker.net/articles/rvalue_references/section_01.html>
* <https://eli.thegreenplace.net/2014/perfect-forwarding-and-universal-references-in-c>
* [Effective Modern C++" by Scott Myers](https://www.oreilly.com/library/view/effective-modern-c/9781491908419/)
* <https://www.artima.com/cppsource/rvalue.html>
* <https://stackoverflow.com/questions/36232671/rvalue-references-what-exactly-are-temporary-objects-what-is-their-scope-an>
* <https://shaharmike.com/cpp/rvo/>