C++ - C++ move semantics - C++ Nanodegree - Tech tutorial - Udacity Instructor Series

C++ the rule of 5 explained.

After learning about Rvalue references in previous posts, the question of the usefulness of such a construct may have arisen. So in this last post we will look in detail at the answer to that question. We will look at the very core of move semantics, namely the move constructor and the move assignment operator. We will extend the already familiar Rule of Three with two more methods to form the Rule of Five and write a class capable of managing a larger block of heap memory in a very efficient manner.

The Move constructor.

With the Move semantics, we can solve two important tasks very efficiently: We can use it to create a deep copy quickly and without extensive recopying, simply by transferring the memory handles from one object to the next. We can also very easily express ownership of data and transfer responsibility for blocks of heap memory. The core idea of move semantics is mainly to move resources between objects rather than copy them. To do this, we need two new constructs to override in our class definition. These are the Move constructor and the Move assignment operator. Both of these together with the three components of the Copy semantics form the Rule of Five mentioned earlier.

Our goal is to customize both the move constructor and the move assignment operator so that a resource-efficient transfer of heap resources can be made between two objects. The important thing here is that the objects themselves are allocated on the stack and manage one or more memory blocks on the heap.

The Move constructor is similar in structure to the Copy constructor, but with two important differences: The parameter is not constant in the Move variant, as well as an Rvalue reference:

It is similar for the move assignment operator:

As in copy semantics, a lvalue reference is returned here. The parameter of the overridden operator is also not constant and is defined as an Rvalue reference.

If we want to create a Deep Copy, then we achieve this in the Move semantics by creating a modified Shallow Copy, in which the memory addresses on the heap are also copied into the target object. n contrast to the regular variant, the pointers to the managed heap resources are marked as invalid after copying in the source object and thus the ownership is transferred from the source object to the target object. There exists at (almost) each time only one reference to the managed resources.

This basic idea is only made possible by the use of Rvalue references. We have seen that the compiler distinguishes whether a Lvalue or a Rvalue is passed as argument. Depending on which case is present, the corresponding variant of the respective function can be called.

In the case of move semantics, this works as follows: If an Rvalue is passed to the Move constructor or to the Move assignment operator, then a Shallow Copy is to be made and ownership of the heap resources transferred from the source to the target. If a Lvalue is passed instead, then a Deep Copy is to be made using the means of Copy semantics without transferring ownership.

Since we usually want to move variables (Lvalues) and not temporary values without identifiers (Rvalues), we face a problem: As we have already seen, we cannot assign a Lvalue to a function with an Rvalue reference as a parameter. So if we want to move a variable from one part of the program to another, we have to pretend to the compiler that we have an Rvalue. To do this, there is the function std::move() that was created in C++11.. With the std::move() function we can convert any value category into an Rvalue reference and pass it to the move constructor or the move assignment operator. We use this to express that the passed variable is no longer needed in the current part of the program, and that the heap resources it manages should be transferred to an object in a new scope.

Let’s take a look at a few examples: If we want to create a new object from an existing object, then we use copy semantics. For example, here we copy the contents of m2 to m1:

In the second case, m1 is used to create a new object m3 with the copy constructor. In both cases, the source object m1 remains unchanged.

If instead we create a new object from an rvalue, then its resources should be moved into the new object. In the example, a new object m4 is created here:

Since we pass a temporary object of type MyClass as an argument, the compiler automatically detects the value category Rvalue and calls the Move constructor. The ownership passes from the temporary object without identifier to the new instance m4. We will look at the exact implementation of the move constructor for this case in detail in the practice section below.

If we want to transfer a variable instead of a temporary object, we have to “wrap” it when calling std::move(). This passes an rvalue reference for the compiler and automatically calls the move constructor. If we omit std::move(), the copy constructor is used instead. So we can use std::move() like a switch to control when to create a copy using the copy semantics and when to use the move semantics to move a resource without extensive copy operations.

Both cases are shown again in the example:

In the definition of m3, the instance m1 is passed as an argument to the copy constructor. In the second case, the instance m1 is converted to an rvalue reference by std::move() and passed to the move constructor of m4.

Since C++11, we have a very efficient way to target the copy processes of objects: If we use std::move(), an object is moved in a resource-efficient way, otherwise a copy of it is created.

The move semantics is usually the better choice as soon as large resources are managed on the heap, or many instances of a class have to be transferred. In fact, in these cases, it is usually more efficient to reassign some pointers than to copy a heap resource each time. If we write our own classes and have an interest in using move semantics, then we need to add both the move constructor and the -assignment operator independently. However, if we use functions from the Standard Template Library, then Move optimizations are usually already included without having to contribute anything special.

We will now write a resource management including fully implemented copy and move semantics.

The “Rule of Five” in practice.

The experiments throughout this article are used to implement and analyze the transfer of heap resources between objects in detail. The preparation portion takes some time, but it’s worth tracing the steps. First, we write ourselves a class called MovableClass and add each component to the Rule of Five. In Listing 17 you can see the full implementation of the class.

Then we define a global variable idCnt that we can use to generate a unique ID for each created instance of MovableClass (→//1).

// 1

int idCnt = 0;

template <class T>

class MovableClass

{

private:

    T *_resource;

    int _id;

public:

    //2

    MovableClass(T *ptr = nullptr)

    {

        _resource = ptr;

        _id = ++idCnt;

        cout << _id << ” constructor “

             << sizeof(*ptr) << ” byte\n “;

    }

// 3

    ~MovableClass()

    {

        cout << _id << ” Destructor “

             << (_resource != nullptr ? …

… “(delete)\n” : “\n”);

        delete _resource;

    }

    // 4

    MovableClass(const MovableClass &source)

    {

        if (source._resource != nullptr) {

            _resource = new T;

            *_resource = *source._resource;

        }

        _id = ++idCnt;

        cout << _id 

   << ” Copy Constr. from “

             << source._id << “\n”;

    }

    // 5

    MovableClass(MovableClass &&source)

    {

        _resource = source._resource;

        source._resource = nullptr;

        _id = ++idCnt;

        cout << _id << ” Move Constr. from “

             << source._id << “\n.”

    }

    // 6

    MovableClass &operator=(const MovableClass &source)

    {

        cout << _id << ” Copy Assignm. from “

             << source._id << “\n”;

        if (&source == this)

            return *this;

        delete _resource;

        _resource = nullptr;

        if (source._resource != nullptr) {

            _resource = new T;

            *_resource = *source._resource;

        }

        return *this;

    }

    // 7

    MovableClass &operator=(MovableClass &&source)

    {

        cout << _id << ” Move Assigm. from “

             << source._id << “\n”;

        if (&source == this)

            return *this;

        _resource = source._resource;

        source._resource = nullptr;

        return *this;

    }

};

*Listing 17 : Class definition considering the “Rule of Five”.

The class has a public and a private section. In the latter, two member variables are defined: An integer _id, which will allow us to keep the individual instances of MovableClass apart via a unique ID, and a pointer _resource to the managed heap resource.

To make the class usable for many different data types, we want to define the resource as a template, i.e. we still leave the data type open in the class definition. To do this, we write the keyword template directly in front of class, followed by . This makes T available everywhere in the class and acts as a placeholder for the data type of the respective instance, which is yet to be defined. We have thus defined a pointer to the heap, which still has to be allocated a memory resource in the course.

The resource is passed to the class when it is instantiated, i.e. we must next adapt the constructor accordingly (→//2). This has as parameter a pointer initialized to zero to the same data type as _resource. Then, the constructor assigns the passed handle to our internal resource. To ensure that each new instance of MovableClass gets a unique ID, the variable _id is also assigned the current value of idCnt and incremented by 1 immediately after the assignment.

Finally, a string with the current object ID and the size of the managed heap resource are printed to the console. While this has nothing to do with the actual move semantics, it will help us keep objects apart when moving resources from one instance to the next.

Next is the destructor (→//3). IThe first thing we do is check the heap pointer for validity. If this points to a valid memory block, then we also print the string “(delete)” to the console. The idea here is to find out which instance manages a resource and which ones are just “empty shells” without a valid heap pointer. After that we have to deallocate the heap resource with delete.

Now to implement the copy constructor (→//4). First, we check whether the source has a valid resource to manage. It could be that its heap pointer points to nothing. If the heap pointer is valid, we first allocate a custom resource named _resource on the heap, using the template type T. Once the memory is allocated, we can copy it from the source to the new resource. This gives us the basic structure of the copy constructor.

The last thing we want to do is set the ID of the current instance and output it to the console, so that we know later on which object the copy constructor was called. The code for this corresponds to the output in the regular constructor. Additionally, we want to know which source object is being copied from. To do this, we output both the ID of the target object and the ID of the source on the console.

Next, let’s look at the move constructor (→//4) for comparison. We have already discussed the two main differences in the call: The parameter here is an rvalue reference and it is not declared as const. Unlike the copy constructor, we don’t have to care about a valid heap pointer in the implementation because we don’t access the memory behind it. It is sufficient if we copy the address of the heap resource instead of its contents, as in a regular shallow copy. Thus we have duplicated the heap pointer of the source object.

Next, to make ownership unique and avoid having two instances pointing to the same heap memory, we still need to set the source pointer to null. By the way, this assignment is the reason why the parameter in the Move constructor must not be declared as const – because otherwise we would not be able to change the pointer in the source object. Finally, we copy the ID-relevant lines from the Copy constructor and adjust them slightly in the output.

The copy and move assignment operators are still missing. Let’s start with the latter (→//5). Iit is important that we return an lvalue reference, which is the reason for the address operator & before operator. If we don’t, then we have one more unnecessary copy operation that needs to be performed. Just as with the copy constructor, the reference to the source is implemented here as a constant lvalue reference.

The first thing we output in the implementation is the ID of the instance, because with the Copy assignment there is a possibility that we return prematurely and therefore don’t get to the end of the implementation. We’ll look at this in a moment in one of the examples. Since we are doing an allocation and not a copy, we must not increment the ID because no new block of memory is allocated on the heap.

We also output the ID of the source. Directly after the output comes the protection against self-allocation. To do this we check whether the source is equal to the current object. If it is, we return the memory contents of this as a value reference. If an object is assigned to itself, then this protection would ensure that no undefined behavior would occur. If we didn’t do this, then depending on the implementation of the copy assignment, an access violation could occur on the heap.

To avoid memory leaks, we next need to free the resource that was previously managed by the current object. To do this, we call delete and set the resource pointer to zero immediately after. After the previous resource is freed, the new resource can be copied from the source. However, to avoid an access violation, we must first check whether the source manages a valid heap resource at all. So the copying should only be executed if the pointer to the heap resource in the source is non-zero. If this is the case, the first thing we need to do is allocate a new memory area and then copy the contents of the source object.

There are also implementations where an existing heap resource is reused or a smart pointer is used instead of new and delete. We don’t do either here, so that the basic principle of the copy assignment is better understood without taking the focus away from the essentials. At the end, the current object must be returned as a value reference with return *this.

At the end of the preparation, the last component of the Rule of Five has to be added (→//7), namely the move assignment operator. First, we change the parameter from an Lvalue reference to an Rvalue reference and remove the const keyword. After that, as with the copy assignment, the ID is output and the protection against self-assignment follows. We do not have to check for validity of the source resource because we don’t access the heap memory directly in the Move assignment. Instead, we simply bend the pointer to the current object and then set it to zero within the source object. After returning the dereferenced pointer, the rule of five for our own class is finally implemented. We can now copy and move instances of MovableClass with a tuned and hopefully consistent strategy.

After this lengthy preparation, let’s look at the behavior of the class in some experiments.

Experiment 1

In the first experiment, we create several instances of MyClass on the stack in Listing 18, passing a handle to a heap resource to the constructor. The first instance mcd should have the data type double (→//8). The second instance mcld, on the other hand, is to be parameterized with the data type long double and thus has a higher memory requirement on the heap. At output 8 we can see that each object has its own ID. In addition it is noticeable that the objects are destroyed in the reverse order in which they were created. This also makes sense, because we have “piled up” all instances on the stack from top to bottom and the stack pointer is reset from bottom to top at the end of main(). We can see that the memory requirements of the managed heap resources correspond to the data type of the respective template.

int main()

{

    // 8

    MovableClass<double> mc_d(new double);

    MovableClass<long double> mc_ld(new long double);

// 9

    MovableClass<double> mc_d2(mc_d);

    MovableClass<double> mc_d3 = mc_d;

    // MovableClass<double> mc_d4(mc_ld); // error

    // 10

    MovableClass<double> mc_d5(std::move(mc_d));

    // 11

    mc_d = std::move(mc_d5);

    return 0;

}

Output 8: 

1 constructor 8 byte

2 Constructor 16 byte

2 Destructor (delete)

1 Destructor (delete)

Output 9: 

3 Copy Constr. from 1

4 Copy Constr. from 1

4 Destructor (delete)

3 Destructor (delete)

Output 10: 

5 Move Constr. from 1

5 Destructor (delete)

1 Destructor

Output 11: 

1 Move Assigm. from 5

5 Destructor 

1 Destructor (delete)

*Listing 18 : Experiments with move constructor and move assignment operator

Experiment 2

In the second experiment, we want to look at different forms of assignment and target the individual components of the class. First, we want to call the copy constructor (→//9). To do this, we instantiate a new object called mcd2 and pass in the object mcd when we define it.

Before we look at the output for this, let’s initialize another object mcd3 in a slightly modified form using the assignment operator. The intent here is the same as with mcd2l: we want to initialize a new object with an existing object. In output 9 we can see that in both cases the copy constructor is called. In the case of object ID 3, this can also be seen immediately, because the syntax is unique in the definition of mcd2. With object ID 4 it is perhaps not so clear, because the assignment operator is involved. However, this is just a slightly different syntax here to illustrate for us what is happening – the new object mcd3 takes over the contents of an existing object.

In the first experiment we created objects with different template data types. If we now try to initialize an object of type long double with an existing object of type double, we get an error message from the compiler:
error: no matching constructor for initialization

If we use templates, then the compiler automatically checks for the validity of the assignment. This makes sense, since the size of the managed heap resource differs by 8 bytes between the sample objects and an assignment from the larger to the smaller object would not be feasible without data loss.

Experiment 3

In the third experiment, we deal with the move constructor. Initializing an object with move semantics means transferring a managed resource from one object to another. With this we express that the object passed as argument should give up its resource and is no longer needed from this point on. We define a new object mc_d5 of type MovableClass and pass as argument to the constructor the instance whose resource we want to transfer (→//10). In order to call the move constructor and not the copy constructor, we usestd::move() to turn the mcd argument into a rvalue reference.

Although the function name “move()” implies that we move something, this is basically not the case. The move constructor of the instance mcd5 is only called by the compiler. The actual moving takes place in the constructor and only has something to do with std::move() only indirectly. In output 10, two things are interesting for us: first, the move constructor is called when the object ID 5 is created, and second, the delete from the previous experiment is missing at the very end when the object ID 1 is destroyed. This means, according to the conditional output in Listing 18, that the object does not have a valid resource pointer – which makes sense, since by passing it from one object to the other we have moved the managed resource, marking the source pointer as invalid.

We can also move a resource back again. To do this, after creating object ID 5, we simply use the assignment operator (→//11) in combination with std::move(). At output 11 we can see that the move assignment operator of object ID 1 has been called. At the end of the output we can further see that the destructor of object ID 5 has been called without “(delete)”. This makes sense, because we have moved the heap resource in object ID 1 with std::move() and the move assignment. The destructor of object ID 1 is called last – as expected with “(delete)” – because this object has also taken over the responsibility for the heap resource.

Experiment 4

In the fourth and last experiment, we will try to pass an object to a function in such a way that the managed resource is transferred in the process. To see what happens to the memory, we copy the printValueAndHandle() function into our class definition in Listing 19. Here we first print the ID of the respective object and the memory address of the resource on the heap. In the second line we output the memory contents, but only if the pointer resource has a valid value – otherwise the value 0.0 is output. Next, we write a function to take an object of type MovableClass.

We’ve read a lot about rvalue references so far, and it’s often assumed (incorrectly) that they should be used in move semantics as parameters when passing arguments. We therefore define the function moveObjectHere1() as a test and set its parameters as Rvalue references. In the function we only call the newly defined member function printValueAndHandle() to output info about the managed object. In main() we call the function (→//1) and pass mcd as an argument using std::move(). One might suspect that after the first call to moveObjectHere1(), the object mc_d might have been moved into the function. In this case, when moveObjectHere1() is called again and the heap resource is output to the console, an invalid memory area should be seen. However, we can see from output 1 that this is not the case: we can still access the heap resource from within main() and neither the address nor the value behind it have been changed by passing it with std::move(). The conclusion from this experiment: to implement move semantics in functions, rvalue references as parameters obviously do not help us.

template <class T>

class MovableClass

{

    void printValueAndHandle()

    {

        cout << _id << ” heap=” << _resource;

        cout << “, *heap=” 

<< (_resource != nullptr ? …

… *_resource : 0.0) << endl;

    }

};

template <class T>

void moveObjectHere1(MovableClass<T> &&obj)

{

    obj.printValueAndHandle();

}

template <class T>

void moveObjectHere2(MovableClass<T> obj)

{

    obj.printValueAndHandle();

}

int main()

{

    // 1

    MovableClass<double> mc_d(new double{10});

    moveObjectHere1<double>(std::move(mc_d));

moveObjectHere1<double>(std::move(mc_d));

    // 2

    moveObjectHere2<double>(std::move(mc_d));

    moveObjectHere2<double>(std::move(mc_d));

    return 0;

}

Output 1: 

1 constructor 8 byte

1 heap=0x7fe70f401770, *heap=10

1 heap=0x7fe70f401770, *heap=10

1 destructor (delete)

Output 2: 

1 Constructor 8 byte

2 Move constructor from 1

2 heap=0x7f9276c01730, *heap=10

2 Destructor (delete)

3 Move Constructor from 1

3 heap=0x0, *heap=0

3 Destructor 

1 Destructor

*Listing 19 : Moving objects with the move semantics

Let’s try something else! Instead of the Rvalue reference we now pass a “conventional” Lvalue. We copy the function moveObjectHere1(), change the name to moveObjectHere2() and the parameter to a lvalue (by removing the double address operator && before the identifier). In main(), we call the new function in exactly the same way (→//2) as in the last example. If we look at output 2, it is clear that there is significantly more going on here than in output 1. In total, three instances of MovableClass are created. The first instance is clear. The second instance is created on the first call to moveObjectHere2(), since we pass the argument using Call-By-Value. From the console output, we can see that _resource contains a valid heap address that points to the value 10. When exiting the function, the object is then destroyed again and from the output “(delete)” we can see that the heap memory is freed in the process.

With the second call of moveObjectHere2() we pass the first MovableClass instance again, but in the meantime it does not manage a heap resource anymore. After all, we moved the resource into the scope of the function in the last step. Accordingly, an invalid heap address and the value 0 are output in the scope of the second function call. For this reason, the two destructors at the end also no longer release any memory, which we can easily recognize by the missing “(delete)”. We have thus seen that an existing function does not have to be adapted if an object is to be passed using Move semantics. The move mechanism takes place automatically at the point where the passed argument is copied into the function parameter.

If we use std::move(), the move constructor will be used in this example without having to modify the function to do so. What this means for us is that we can take advantage of the performance benefits of move semantics in older code without much additional effort. As soon as we tell std::move() that we want to move a resource, the compiler will use the move components of the Rule of Five.

Conclusion

We have seen how Move semantics can be used to transfer resources. We used the Move constructor and the Move assignment operator to move the Rule of Five implemented heap resources from one part of the program to another. This is basically the heart of Move semantics – move quickly instead of copying expensively.

Speed and efficiency has always been one of the main advantages of C++ over other programming languages. And with the introduction of Move semantics, this advantage has become even more apparent.

Develop your C++ skills.

C++ is a compiled, high-performance programming language. Robots, automobiles, and embedded software all depend on C++ for speed of execution. Interested in learning more about C++? Get hands-on coding experience with Udacity’s C++ nanodegree. Start learning online today!