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

Master C++ copy semantics.

While C++ has always been known for high-performance and fast programs, the introduction of C++11 has made the language even more efficient – not least with the introduction of move semantics. This powerful tool allows us to prevent the unnecessary creation of temporary objects in many places in our code, as well as make the passing of large data types from one scope to the next much more efficient. 

Unfortunately, move semantics has a reputation for being very complicated – not least because of the concepts involved, such as rvalue references or the move constructor. The goal of this blog series is therefore to convey the main ideas of Move semantics in a practical and easy-to-understand way. We will look at many examples and use carefully selected experiments to illustrate the basic mechanisms. 

The blog series consists of four parts: In the first part (this article), we look at copy semantics, an important foundation for move semantics.  The next articles will deal with the concept of Lvalues and Rvalues, with Rvalue references, and finally with the famous Rule of Five, the heart of move semantics.  With the knowledge gained from this series of articles, it is possible to optimize existing code as well as to take move semantics into account already during class design when creating new programs.

Introduction to C++ copy semantics.

This article is about strategies when copying objects. We will see how we can target the transfer of data from one object to another using mechanisms such as Shallow-Copy and Deep-Copy. One rule is very important here, the so-called Rule Of Three. We will take a close look at the destructor, the copy constructor, and the copy assignment operator and learn how we can adapt these components to our needs in order to manage heap memory safely.

Shallow copy vs. deep copy.

When we write our own classes and instantiate and copy objects in the course of the program, these operations are performed according to a default set in the compiler – without having to explicitly specify how exactly, for example, a copy operation should look in detail. In many applications, however, this default scheme leads to errors or makes the code inefficient. The goal of copy semantics is therefore to specifically influence the behavior of objects over their entire life cycle (instantiation, copying, destruction) and to adapt them to one’s own requirements.

Shallow copy.

Generally one can distinguish two kinds of copies: In a Shallow Copy (“flat” copy), all data of an object are copied 1:1. If, for example, a class consists of a mixture of stack variables and pointers to heap memory, then in this copy variant the contents of the stack variables and the addresses of the pointer variables are transferred to the copy. While the stack variables in the original and copy are completely independent of each other, there is a risk of an access conflict when accessing the heap memory because the same heap address exists in both instances. The following graphic shows a class instance and its copy that refers to the same heap memory blocks (obj1-3) via pointer variables (ptr1-3).

Shared heap access with shallow copy.

Deep copy.

The second variant for copying data is called Deep Copy. The basic difference to ShallowCopy is that here the contents of heap variables are duplicated completely. To realize this, new heap memory is reserved during copying and the contents of the original are stored in it as an independent copy.

The diagram below shows two instances of a class, the one on the right having emerged as a deep copy from the one on the left. In a deep copy, we are protected from access violations because the heap memory is duplicated. In the graphic you can see that each object has its own heap variables that are not related to each other.

Separate heap access with deep copy.

Even though there is a risk of access conflicts with shallow copy, deep copy is not flatly better. Sometimes it is intentional that changes on the heap are seen by multiple objects. Also, reserving memory on the heap and copying data is costly. If we have many objects or large data, then this can quickly become a performance problem that slows down our program. 

Customizing class components.

If we write a class and don’t worry about copying at all, then the compiler will generate a shallow copy as explained earlier. If we don’t want that and would rather have a deep-copy, then we need to customize our class accordingly. This process of customizing is called copy semantics. Here we need to customize a total of three components of our class:

The first component is the destructor:

This controls what happens to our data when the object is deleted: Are the heap variables also deleted and the memory released again? Or should they better be kept because they might still be used by other objects?

The second component is the copy constructor:

If an object is copied with this, then no changes to the original are possible. To achieve this, a const reference to the source is passed as a parameter when calling the copy constructor – the difference between Call-By-Value and Call-By-Reference is assumed to be known here. We will see in a moment in the practical part how the copy constructor can be called and used.

The third component is the copy assignment operator

In terms of content, this is largely the same as the copy constructor. However, the real difference lies in the way it is used in the code. The combination of the keyword operator and the operator = indicates that for the class

MyClass the assignment operator should be overridden. It is also interesting to note that the return type is created by using & as a reference. We will look at this in more detail in the next article on Lvalues and Rvalues. The parameter in parentheses is identical in content to the copy constructor.

Since a total of three components must overwrite, this is often referred to as the Rule of Three. The rule states that classes in which at least one of the three components is overwritten must also adjust the others to avoid memory management errors. The rule is intended to prevent access conflicts from occurring when reading and writing to the heap, or even memory leaks. For example, if it is forgotten to adjust the copy assignment operator, there may be differences in copy behavior depending on the syntax when passing or copying objects. In the practical part we will look at some examples of this in detail.

Copy semantics in practice.

We have learned so far that the compiler creates a Shallow Copy by default when copying objects, copying addresses of heap variables 1:1. In this practice section, we will look at the different copy variants in detail, adapting the components of the Rule of Three to our own requirements.

class MyClass
{
public:

MyClass()
    {
        cout << "Constructor\n";
    }

    ~MyClass()
    {
        cout << "Destructor\n";
    }

    MyClass(const MyClass &source)
    {
        cout << "Copy Constructor\n";
    }

    MyClass &operator=(const MyClass &source)
    {
        cout << "Copy Assignment\n";
        return *this;
    }
};

Listing 1: Custom class with conformance to Rule of Three.

Preparation

Before we can start experimenting, let’s first write a class in which the destructor, copy constructor and copy assignment operator are overridden and output appropriate text to the console when they are called. Our intention here is to figure out which method is called in which situation.

In Listing 1, you can see the implementation of the MyClass class, which overrides every component of the Rule of Three, including the constructor, and produces output on the console when it is called. As explained, the copy assignment operator passes a reference to the return value to the calling page. This avoids a copy operation. With return this we write the content from the source instance directly into the memory content of the target object. We’ll look at what happens when we make the return as *return-by-value without the address operator in a moment in the examples.

Experiment 1

In the first experiment, we want to have the overwritten components of our class called.

int main()
{
    // 1
    MyClass m1, m2;

    // 2
    m2 = m1;

    // 3
    MyClass m3(m1);

    // 4
    MyClass m4 = m1;

    return 0;
}

Output 1: 
Constructor
Constructor
Destructor
Destructor

Output 2a: 
…
Copy Assignment
…


Output 2b: 
…
Copy Assignment 
Copy Constructor
Destructor
…

Output 3 / 4: 
…
Copy Constructor
Destructor
…

Listing 2: Calls to components of the Rule of Three.

First, we create two instances of MyClass` named m1 and m2 (→//1) in Listing 2 for this purpose. In the corresponding output 1 we can see that for each object the constructor and the destructor are called. 

If we now assign one object to the other object (→//2), then we can see in output 2 that the copy assignment operator is called and the contents of m1 are copied to the memory location of m2. The output of both constructors and destructors as seen in Output 1 has been replaced with “…” here and in the other outputs of Listing 2 for clarity. 

If we test by removing the reference at the return value of the copy assignment operator in the class definition of MyClass in Listing 1 and thus trigger a Return-By-Value, then we see in Output 2b that besides the copy assignment also the copy constructor as well as an additional destructor are called. The reason for this is that a temporary return object is created and immediately destroyed after the content is copied into m2. Compared to the previous Return-By-Reference, this is very inefficient and the reason why we reverse the change in Listing 1 for the rest of the experiments.

Now let’s take a closer look at the copy constructor, and to do so we define a new object m3 in (→//3), which we initialize with m1. In the corresponding output 3 we can see that the copy constructor is called as expected. This is to copy the contents of m1 into m3 (implementation to follow shortly). 

The call in (→//4) is interesting. Here, a new object m4 is apparently created and overwritten with the contents of m1 by the assignment operator. However, we can see from the output that the copy constructor was called here instead. The reason for this is that the functionality of the statement in (→//4) is basically no different from that of the assignment operator: A new object is created and immediately initialized with the contents of another object. The alternative syntax with the supposed assignment operator essentially serves to make the code more readable for the programmer.

Experiment 2

So far, we have already called all four overridden components of the class in various situations. In the next step, we want to extend the class to manage one resource on the stack and one on the heap. In the first part, we will implement the copy semantics in our class in such a way that a shallow copy is created.

In Listing 3 the code from the preparation was taken over and the identifier MyClass was replaced by MyCopy. We do not override the copy constructor and the copy assignment operator in this version, so the compiler will automatically generate the default variants. In addition, two resources in the public part of the class (→//1) have been supplemented by a pointer variable with the identifier _myNumber and a string with the identifier _myName. In the constructor, a block of memory is allocated on the heap for _myNumber in addition to the console output (→//2). 

Since we are responsible for managing heap memory when reserving it, we must release the block allocated with new by calling delete in the destructor of the class (→//3). 

Additionally, a function printValueAndHandle() is included (→//4), which prints the name of the respective object as well as the stack address of the managed double value on the heap.

class MyCopy
{

public:
// 1
    double *_myNumber;
    std::string _myName;

// 2
    MyCopy()
    {
        cout << "Constructor\n";
        _myNumber = new double{0.0};
    }

    // 3
~MyCopy()
    {
        cout << "Destructor\n";
        delete _myNumber;
    }

// 4
void printValueAndHandle()
    {
        cout << _myName << " heap address = " 
<< _myNumber;

        cout << ", value = " 
<< (_myNumber != nullptr ? 
*_myNumber : 0.0) << endl;
    }

};

Listing 3: Implementation of a simple shallow copy.

The output of the heap contents of _myNumber depends on whether memory has been allocated at all: if _myNumber points to a valid memory area, then the corresponding value is output, otherwise 0.0 is written to the console.

Now that we’ve implemented the class so far, we’ll create a few instances of it in Listing 4 and see what happens. 

First, we create an instance s1 of MyCopy on the stack in (→//5). Then we set the internal string to s1 and the double value to 42. Following this, we create a second instance s2 in the same way and overwrite its member variables with our own values so that we can tell the different instances apart in the console output.

Next, the printValueAndHandle() method is called for both instances (→//6) so that we can see the value and address of the managed double variable. From output 1 in Listing 4, we can see that instances s1 and s2 manage two different heap resources, differing in both address and value.

int main()
{
// 5
    MyCopy s1;
    s1._myName = "s1";
    *s1._myNumber = 42.0;

    MyCopy s2;
    s2._myName = "s2";
    *s2._myNumber = 23.0;

// 6
    s1.printValueAndHandle();
    s2.printValueAndHandle();

// 7
    s2 = s1;
    s2.printValueAndHandle();
}

Output 1: 
Constructor
Constructor

s1 heap address =    0x7fdd2d401710,    
       value = 42

s2 heap address = 
       0x7fdd2d401720, 
       value = 23

Destructor
Destructor

Output 2: 
…
s1 heap address =    0x7fc2e66004c0, 
       value = 42
s2 heap address = 
       0x7fc2e66004d0, 
       value = 23
s1 heap address = 
   0x7fc2e66004c0, 
       value = 42
…
/bin/sh: line 1:  3839 Abort trap: 6  
exited with code=134

Listing 4: Experiments with a simple variant of the shallow copy.

Now let’s see what happens when we cause a copy assignment according to the compiler default. To do this, we assign instance s1 to instance s2 (→//7), and output the value and handle of s2’s double member variable on the heap immediately afterward. The resulting output 2 is very interesting because several things are possibly unexpected: The first thing that stands out is the text output after the copy assignment: the instance s2 has now been assigned both the value of the string and the address of the double variable. This means that we now have two instances pointing to the same heap resource. 

The next perhaps unexpected aspect is the program crash when calling the second destructor. The reason for this is that the first destructor uses delete to free the heap memory again. But since the second handle in the other instance points to the same resource, calling delete again causes a crash – because the memory has already been freed.

Such inconsistent memory management is one of the problems that quickly arise with default implementations. If we take a closer look at Listing 4, we see that the Rule of Three has been violated here: We have adjusted the destructor without taking care of the copy assignment operator or the copy constructor – and the consequence is a program crash in this case.  

Experiment 3

In the last experiment, we will extend the MyCopy class and add the missing components of the Rule of Three. Listing 5 shows the corresponding implementation of the copy constructor and copy assignment operator. 

In the copy constructor, analogous to the regular constructor, a new block of heap memory is reserved with new by the target instance. Subsequently, the content of _myNumber is copied from the source instance. 

No new memory needs to be reserved in the copy assignment operator, because the target object already exists and has its own heap memory. So at this point only the content is copied and finally with return this the complete content of the target object is returned by *Return-By-Reference.

By adding both components and thus completing the Rule of Three, we now have a class that creates a deep copy of its instances.

So each object gets its own memory areas on the heap and is completely independent of other objects.

In the output, we can see that only the contents of the double resource were copied into s1, but not its memory address. Normally, we would have to add a mechanism at this point that protects us from assigning an object to itself – but we will discuss this in a later article. 

So by applying the Rule of Three, we were able to implement our own copy strategy, which replaced the default Shallow Copy with a Deep Copy.

In a later article, we’ll add two more components to our class to form the Rule of Five – a fundamental rule in move semantics.

…
MyCopy(const MyCopy &source)
    {
        cout << "Copy Constructor\n";
        _myNumber = new double{0.0};
        *_myNumber = *source._myNumber;
    }

    MyCopy &operator=(const MyCopy &source)
    {
        cout << "Copy Assignment\n";
        *_myNumber = *source._myNumber;
        return *this;
    }
…

Output: 
Constructor
Constructor
s1 heap address = 
0x7fde67c01710, 
value = 42
s2 heap address = 0x7fde67c01720, 
value = 23
Copy Assignment
s2 heap address = 0x7fde67c01720, value = 42
Destructor
Destructor

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. Explore the C++ Developer Nanodegree to expand your software engineering skills into C++ development.

START LEARNING