Chapter 6 : The Philosophy Of C++, Part B

Key words: Object management in C++, constructors, destructors, object’s life-cycle, the principle of Resource Acquisition Is Initialization (RAII), the fundamentals of copy and move semantics in modern C++, copy constructor, copy assignment operator, shallow copy vs deep copy, compiler default copy , r-value and r-value reference in modern C++, move constructor, move assignment operator

Topics at a glance

  • Introduction to object management
  • The beauty of creation with constructors
  • The horrors of wandering : Resource leakage, destructors to the rescue
  • Virtual destructors and their significance
  • The principle of Resource Acquisition Is Initialization (RAII)
  • Establishing class invariants
  • Making copies via copy constructors and copy assignment operators
  • The dangers of shallow copy and understanding deep copy
  • Moving objects via move constructors and move assignment operators
  • Compiler defaults, their vagaries in copying and moving objects

Object Management in C++

Here, I want to show you the C++’s elegant mechanisms for object management. First let us see how an object’s life-cycle management is done through constructors and destructors, and then, how objects can be copied and moved around using modern C++’s copy and move semantics. Then we’ll see the famous C++ idiom Resource Acquisition Is Initialization abbreviated as RAII.

Object’s life cycle

Constructors

Let us discuss about some of the important aspects related to constructors. We all know that constructor initializes the object’s data members during object creation. Constructor is invoked during object creation and this is guaranteed by C++.

Default No-Argument Constructor

Even if your class doesn’t have an explicit constructor of its own, compiler will write a default no argument constructor for your class and invokes this upon object creation. This automatic insertion of constructor by the compiler will ONLY happen if you don’t have your own explicit constructors for your class.  If you want C++ compiler to insert default no argument constructor, then use you can ask the compiler to make one for you using ‘=default’. See the code snippet below.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
class A
{
private:
    int a;
public:
    A() = default;
    A(const int val): a{val}
    {
        cout << "A created: " << val << endl;
    }
};

One Argument Constructor and its Implicit Behavior

The constructor A(const int val)  is a little tricky as it has got an implicit behavior. This constructor will get invoked automatically if you try to assign a value of the type of the argument in that one argument constructor.

Let’s see how this will work for the above class A:

A a = 10;

Result:

A created : 10 

Result implies A(int) is getting called when you try to assign a value of type ‘int’. This will create an object ‘a’ of class ‘A’ from an int 10. Code like this is difficult to read, maintain and sometimes can lead to bugs. Also, why to do an implicit conversion from integer to an object of type ‘A’. To stop this implicit behavior, make use of ‘explicit’ keyword in your one argument constructor, as shown below:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
class A
{
private:
	int a;
public:
	A() = default;
	explicit A(const int val): a{val}
	{
		cout << "A created : " << val << endl;
	}
};

The code “A a = 10” will not even compile now. The following error is thrown by g++, when you try it:

error: conversion from 'int' to non-scalar type 'A' requested
  A a = 10;

Constructor Delegation

What if, your class has many variants of constructors? For example, see the code below:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
class A
{
private:
    int a;
    int b;
    int c;
public:    
    A(): a{0}, b{0}
    {
        // do nothing 
    }
    
    explicit A(const int val1)
    {
        a = 0; // this code is redundant as A() does this
        b = 0; // this code is redundant as A() does this
        c = val1;
        cout << "A created : " << a << ", " << b << ", " << c << endl;
    }
};

In the above code setting ‘a’ and ‘b’ to zero is done by A(). So, similar code in A(int) is redundant. We can use C++ constructor delegation to avoid code duplication in A(int).

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
class A
{
private:
    int a;
    int b;
    int c;
public:    
    A(): a{0}, b{0}
    {
        // do nothing 
    }
    
    explicit A(const int val1) : A() //, c{val1} 'c' can be initialized like this also
    {
        c = val1;
        cout << "A created : " << a << ", " << b << ", " << c << endl;
    }
};

Now lets see how it works when you run the following code :

A a(20);

Result:

A created : 0, 0, 20

Derived Classes Order of construction:

In case of an object of a derived class, the order in which constructors are called will be always starting from base class constructor followed by derived class constructor. Makes complete sense, right? Base class is also called the parent class, and derived class is also known as child class. In that case, without parent object creation, how the child object could be created, as derived class object is partly base class object as well. It is programmer’s responsibility to invoke the correct variant of base class constructor, providing the correct initial values. C++ compiler can guarantee to invoke only the default no argument constructor of base class. If your class doesn’t have a no argument constructor, then its programmer’s responsibility to invoke the correct constructor explicitly through constructor delegation. i.e. in a derived class, base class constructor can ONLY be invoked in derived class’s constructor’s member initializer list as shown below:

See the code below:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
#include <iostream>

using namespace std;

class A
{
private:
    int a;
public:    
    A(): a{0}
    {
        // do nothing 
    }
    
    explicit A(const int val1): a{val1}
    {
        cout << "A created : " << a << endl;
    }
};

class B : public A
{
private:
    int b;
public:
    B(int val1, int val2) : A(val1), b{val2}
    {
        cout << "B created : " << b << endl;
    }
};

int main()
{
    B b(10, 20);
    cout << "End of main" << endl;
    
    return 0;
}

Next, I will move on to destructors. Some of you might wonder why there is no mention about copy constructors and move constructors here? I will explain this topic later in this chapter.

Destructors in C++

I think in C++, destructor is one of the overlooked topic. Most of the beginners think destructors are there just for cleaning up the object, and everything will be taken care of automatically.  There is no need to worry about them, as the program is robust enough to handle things when objects live. When they die who cares. This is the general attitude towards destructors. But unlike other programming languages where the memory (heap) is managed by some dedicated background tasks as garbage collector in Java, in C++, it is programmer’s responsibility to manage heap. Mange heap in short, covers the course of actions you have to perform in getting memory from heap, using them in your program, and finally returning them back to heap. In C++, heap management is abstracted by an entity called the free store and primarily through free store’s new’ and ‘delete‘ operators. So, you may ask, why in C++ these things are programmer’s headache. C++ abstractions let you work right on top of the hardware without much software layering, unlike other programming languages like Java, C# and other sophisticated frameworks. You can use those languages/frameworks for certain kind of applications, but when it comes to performance critical, real time application development C and C++ are the obvious choice. Programming real time systems, OS core libraries, game engines, safety critical software that goes into automobiles, avionics systems, embedded systems such as IOT enabled devices, mission critical software used in rovers, medical appliances, life supporting devices etc are just a few that uses C and/or C++. Also, garbage collectors only guarantees to free the memory back to heap and they don’t really care about other type of resources that you have used in your object’s life-cycle, such as threads, open files, open sockets and the list goes on. i.e. Memory is not the only resource that a program manages. Such resources are still programmer’s responsibility to take care.

Now, let us see what’s so special about destructors. As you know destructors do the essential cleanup at the end of an object’s life-cycle, while constructors do the necessary initialization when the objects come alive.  But one thing you clearly need to understand is that, C++ only guarantees to invoke constructors and destructors when objects come alive and when objects life cycle come to an end, respectively. What should happen inside constructors and destructors is all up to the programmer to decide. Most of the programmers are keen in setting up the initialization values in constructors, getting resources such as threads, files, sockets etc., acquiring their handles, getting memory from heap etc. But often forget to clean them up – close open handles, return from (join) a thread etc. Some programmers do take care of such things but not in the destructors. They do these inside some other member functions and IF everything goes as planned, then their custom clean-up routine will take care of releasing the resources. What if, there is an unexpected condition, and an exception is thrown in course of execution of your program? Then there is no guarantee that your routine will ever be executed at all, and in that who will take care of those resources?

I will demonstrate the issue with the help of a small code.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
class A
{
private:
    int *memory;
public:
    A(int size) : memory{new int[size]}
    {
        cout << "A is created!" << endl;
    }
    
    ~A()
    {
        cout << "A is destroyed" << endl;        
    }
    
    void do_something()
    {
        cout << "Operation not supported or something went wrong!" << endl;
        throw 1;
    }
    
    void cleanup()
    {
        if(memory != nullptr)
        {
            delete []memory;
        }
    }
};

void function_1(void)
{
    A a(10);
    
    a.do_something();
    
    a.cleanup();
    
    cout << "function_1 ends here" << endl;
    
    return;
}

Let us see what will happen?

A is created!
Operation not supported or something went wrong!
terminate called after throwing an instance of 'int'
      1 [main] destrcutors_example 12036 cygwin_exception::open_stackdumpfile: Dumping stack trace to destrcutors_example.exe.stackdump

Let us analyse what went wrong here:

  1. Executing do_something() resulted in throwing an exception
  2. C++ performed the usual stack unwinding, and found there is no code written by the programmer to handle this exception. C++ decide to call the terminate program and dumped the stack.

So, here the real unseen problem is, upon creation, object a, has acquired memory from heap. Just look at A’s constructor. But programmer has taken care of cleaning the memory in an explicit routine called cleanup(). But the problem is cleanup is never called because of an exception, and the memory is never released back to heap. Is that a leak ? Yes.

Let us use the elegant destructor now cleaning this mess up. Shall we?  

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
class A
{
private:
    int *memory;
public:
    A(int size) : memory{new int[size]}
    {
        cout << "A is created!" << endl;
    }
    
    ~A()
    {
        cout << "A is destroyed" << endl;
        
        if(memory != nullptr)
        {
            delete []memory;
        }
        
    }
    
    void do_something()
    {
        cout << "Operation not supported or something went wrong!" << endl;
        throw 1;
    }
};

void function_2()
{
    A a(50);
    
    a.do_something()
    
    cout << "function_2 ends here" << endl;
    // let us hope C++ will call the destrcutor at the end of function_2
    
    return;
}

Let’s see what will be result of executing function_2()?

A is created!
Operation not supported or something went wrong!
terminate called after throwing an instance of 'int'
      2 [main] destrcutors_example 10916 cygwin_exception::open_stackdumpfile: Dumping stack trace to destrcutors_example.exe.stackdump

Again the same thing happened. No proper cleanup. Here, what went wrong? C++ guarantees to call destructor whenever the object goes out of scope. This special code is inserted at the end of function_2(). But it never reached this point and before reaching there, it was terminated.

Let us see now how to handle this situation with a neat trick.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
void function_3(void)
{
    A a(50);
    
    try
    {
        a.do_something();
    }
    catch(...)// catch all exceptions 
    {
        cout << "some exception caught" << endl;
    }
    
    cout << "function_2 ends here" << endl;
    
    return;
}

So, now what happened?

A is created!
Operation not supported or something went wrong!
some exception caught
function_2 ends here
A is destroyed

That’s it! There is a proper cleanup, destructor is called and the memory is returned back to heap. What’s the difference here?

  1. The main difference between function_2 and function_3 is in the way both dealt with exception.
  2. In ‘function_3’ an exception handler is installed using try-catch block.
  3. That caught the exception in function_3’s scope itself and resulted in full execution and return from function_3.
  4. At the end of function_3 C++’s special code to clean-up the object ‘a’, through its destructor executed and resulted in returning the memory back to heap.

noexcept keyword in C++

If you are designing a class, make sure member functions performing complex tasks involving pointer/resource manipulations are tested thoroughly. If your function doesn’t throw any exceptions, then declare it ‘noexcept’. But the problem in ‘noexcept’ is that even in case your member function results in an actual exception, no exception handlers installed in the code can ever catch that exception as the function is declared ‘noexcept’.

In the below code, class A is updated to add do_something_else() which will not throw any exceptions (well, ideally).

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
class A
{
private:
    int *memory;
public:
    A(int size) : memory{new int[size]}
    {
        cout << "A is created!" << endl;
    }
    
    ~A()
    {
        cout << "A is destroyed" << endl;
        
        if(memory != nullptr)
        {
            delete []memory;
        }
        
    }
    
    void do_something() 
    {
        cout << "Operation not supported or something went wrong!" << endl;
        throw 1;
    }
    
    void do_something_else() noexcept
    {
        // do something else 
        return;
    }
};

Virtual destructors in C++

We all know when a destructor is declared as virtual it will become virtual destructor. So, what is it and why we need one. You remember the run time polymorphism I have detailed in previous chapter? Same things are applicable here. Let there be a class hierarchy with base and derived classes, and suppose you have created an instance of derived class, and your client code adheres to code to base class principle. Then, without virtual base class destructor, there is no way C++, is going to call the correct destructor and here, in this case, the correct destructor is the one defined in derived class as the object is a derived class instance.I will show this with a small example.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
#include <iostream>

using namespace std;

class A
{
private:
    int *memory;
public:
    explicit A(int size) : memory{new int[size]}
    {
        cout << "A is created!" << endl;
    }
    
    ~A()
    {
        cout << "A is destroyed" << endl;
        
        if(memory != nullptr)
        {
            delete []memory;
        }
        
    }
    
    virtual void do_something() 
    {
        // do something 
    }
    
};

class B: public A
{
private:
    int *memory;
public:
    B(int size): A(size), memory{new int[size]}
    {
        cout << "B is created" << endl;
    }
    
    ~B()
    {
        cout << "B is destroyed" << endl;
        if(memory!= nullptr)
        {
            delete []memory;
        }
        
    }
    
    void do_something() override
    {
        return;
    }
    
};

// client code takes the ownership of 'a'
// conside main is just creating the object and giving
// complete control of the object's lifetime to client_function
void client_function(A* a)
{
    a->do_something();
    //'a' is to be deleted 
    delete a;
    
    cout << "\nclient code execution done" << endl;
    return;
}

int main()
{
    B *b = new B(10);
    client_function(b);
    
    cout << "End of main" << endl;
    
    return 0;
}

Result:

A is created!
B is created
A is destroyed
client code execution done
End of main

So, who is going to destroy B, and release back B’s memory ? This is because A’s destructor ‘~A()’ was never declared virtual, so it does not have polymorphic behavior and it’s not in v_table. So C++ when it see’s delete operation on a pointer of type A will only call A’s destructor and will not call B’s destructor. If the type itself was B, then B’s destructor will be called first and then A’s destructor in the order. But that is not the case here as client code program assumes the type of the object that it deals with is an A’s instance. And client has complete ownership of object. To avoid such a scenario, we always have to define virtual destructors in base class.

See virtual destructor in action below:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
#include <iostream>

using namespace std;

class A
{
private:
    int *memory;
public:
    explicit A(int size) : memory{new int[size]}
    {
        cout << "A is created!" << endl;
    }
    
    virtual ~A()
    {
        cout << "A is destroyed" << endl;
        
        if(memory != nullptr)
        {
            delete []memory;
        }
        
    }
    
    virtual void do_something() 
    {
        // do something 
    }
    
};

class B: public A
{
private:
    int *memory;
public:
    B(int size): A(size), memory{new int[size]}
    {
        cout << "B is created" << endl;
    }
    
    ~B()
    {
        cout << "B is destroyed" << endl;
        if(memory!= nullptr)
        {
            delete []memory;
        }
        
    }
    
    void do_something() override
    {
        return;
    }
    
};

// client code takes the ownership of 'a'
// conside main is just creating the object and giving
// complete control of the object's lifetime to client_function
void client_function(A* a)
{
    a->do_something();
    //'a' is to be deleted 
    delete a;
    
    cout << "\nclient code execution done" << endl;
    return;
}

int main()
{
    B *b = new B(10);
    client_function(b);
    
    cout << "End of main" << endl;
    
    return 0;
}

Result:

A is created!
B is created
B is destroyed
A is destroyed

Resource Acquisition Is Initialization (RAII)

In the above code, did you notice that in class A’s constructor A(int size), memory is acquired from heap? Also, in A’s destructor the acquired memory is returned back to heap. This is RAII all about. That is to acquire the resource in constructor and to release it, cleanup in destructor, thus avoiding ‘naked new‘ and ‘naked delete‘ .i.e. RAII is making use of two things that C++ guarantees.

  1. C++ guarantees to invoke the constructor at object’s creation
  2. C++ guarantees to invoke the destructor when object’s lifecycle ends

The above two things are mechanisms of C++. RAII is a policy which puts to use these mechanisms in a very elegant and smart way. It is RAII and not MAII. That means, its applicable for any resource, and is not just limited to memory, but extends it to other resources such as threads, files, sockets etc. Bjarne Stroustrup has discussed about this topic nicely in his books – “A Tour of C++” and “The C++ Programming Language”.

Here are the important aspects of RAII

  1. Resources should be acquired as part of constructing the object itself. i.e in constructors.
  2. Constructors should implement class invariants or some condition checks that will prevent any invalid resource acquisition. For instance, in case of memory slots, the invariant can be whether the size parameter is not negative.
  3. In case the check fails, constructors should throw exception, thus indicating object creation has failed.
  4. And lastly, any acquired resource must be cleaned up, released in the destructors, in the reverse order that they were acquired in constructors. Last acquired should be released first.

Thus, RAII is all about managing resources as part of object’s life-cycle itself.

The fundamentals of copy and move semantics in modern C++

Copy and Move Semantics in modern C++

What does it mean to copy and move objects? In the literal sense, copy mean to clone an object to a new object, and make an exact replica of the original object. If object ‘a’ of class ‘A’ is copied to object ‘b’, then object ‘b’ can be treated as an exact clone/replica of object ‘a’. For copying objects, there are two ways in C++.

  1. To create a new object as a clone of an existing object
  2. To copy the contents of an existing object to another existing object, thus making a clone.

Copy constructor:

In the first approach, since a new object has to be created, we will go with constructors as it is the only way to create an object. But here, we are creating a clone or copy of another existing object. So, this special constructor should take this existing object as its parameter and read the contents of that object to this newly created object byte by byte. I repeat byte by byte, as we don’t want a shallow copy but a deep copy. This special constructor is known as copy constructor in C++. I’ll demonstrate this with an example. I will walk you through a small Array class to demonstrate copy constructor.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// copy constructor
Array(const Array& other_object)
{
	cout << "Array object's clone is created" << endl;
	size_ = other_object.size_;
	memory = new int[size_];
	
	// start copying byte by byte
	for(int index = 0; index < size_; ++index)
	{
		memory[index] = other_object.memory[index];
	}
}

Did you notice a special constructor in the code above?

1
Array(const Array& other_object);

The constructor takes exactly one argument, a constant reference to another object of the same class. Let’s examine what all things it is doing inside:

  1. Copies the size_ data from the other object
  2. Acquires a new memory region from free store using new operator to store size_ number of integers.
  3. Copies the data stored in other object’s memory one by one.

Steps 2 and 3 are more important here, as it is just not a shallow copy where the other object’s memory address is alone copied. In deep copy, we allocated a new memory and then copied the members from other objects memory one by one. This is important to note here. i.e the difference between shallow copy and deep copy. Now, let us see the result of the following:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
Array a(10);

for(int index = 0; index < a.size(); ++index)
{
	a[index] = index;
}

// lets copy construct object 'b' from 'a'
Array b(a);

for(int index = 0; index < b.size(); ++index)
{
	cout << "b[" << index << "] : " << b[index] << endl;
}
Array is created with size 10
Array object's clone is created
b[0] : 0
b[1] : 1
b[2] : 2
b[3] : 3
b[4] : 4
b[5] : 5
b[6] : 6
b[7] : 7
b[8] : 8
b[9] : 9
Array is destroyed
Array is destroyed

The default copy constructor

If you don’t write a copy constructor for your class, C++ compiler will always insert one default copy constructor in your class that will look exactly same as the one you have explicitly written above, with ONLY ONE difference. i.e. The default copy constructor can copy only the objects data members. In this case Array class has only two data members declared; one is a ‘size_’ data which is just 4 bytes-integer, and other one is a pointer data ‘memory’, which is another 4 bytes. So, default copy constructor will just copy these 8-bytes from other object to the newly created object. In effect, both the objects will point to the exact same memory region in heap. Thus, the new object is not a clone of the other object or we can say that copy did not work. This is a typical example of shallow copy.

Shallow copy vs deep copy

I will show you the difference between shallow copy and deep copy with the help of diagrams.

The image above is how shallow copy works. C++ default copy constructor does this.

The above diagram shows how a deep copy construction is performed.

Copy assignment operator’=’

This is the second approach I have mentioned above, where there is no new object created. What happens is, an existing object will get copied to another existing object. So, if there is some data already present in the copied to object it will be all lost and will get updated with data inside copied from object. Make sure in the copied to object there is no data present or if there is some data then it should be taken care before copy.

I will demonstrate how copy assignment operator is done in C++.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
// copy assignment operator 
Array& operator=(const Array& other_object)
{
    if(this == &other_object)
    {
        return *this;
    }
    
    cout << "Array object is being cloned" << endl;
    
    // dont allocate a new memory, 
    // use the heap memory allocated to existing object
    
    size_ = other_object.size_;
    // Copy byte by byte to memory
    for(int index = 0; index < size_; ++index)
    {
        memory[index] = other_object.memory[index];
    }
    
    return *this;
}

Now what will happen if you do the following?

1
2
3
4
5
6
7
8
Array c(10);
// 'c' is already created here
c = a;

for(int index = 0; index < c.size(); ++index)
{
	cout << "c[" << index << "] : " << c[index] << endl;
}

Want to see the result? Before that did you notice something – during copy assignment, I did not allocate a new memory for the existing object as memory is already allocated, to the object. Now, let’s see the result of the above code:

Array is created with size 10
Array object is being cloned
c[0] : 0
c[1] : 1
c[2] : 2
c[3] : 3
c[4] : 4
c[5] : 5
c[6] : 6
c[7] : 7
c[8] : 8
c[9] : 9

You can see that object ‘c’ has become a clone of object ‘a’ as a result of copy assignment operation. But here, there is an issue. What if the size of the copied from is greater than that of the existing object’s ? Say, I have an object of 20 integers and tried to copy to another object created for storing just 10 integers. We can have a modified copy assignment operator.

Let us see a modified version of copy assignment operator for our class Array.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
Array& operator=(const Array& other_object)
{
    if(this == &other_object)
    {
        *this;
    }
    cout << "Array object is being cloned" << endl;
        
    if(size_ < other_object.size_)
    {
        delete []memory;
        memory = new int[other_object.size_];
    }
    
    size_ = other_object.size_;
    // Copy byte by byte to memory
    for(int index = 0; index < size_; ++index)
    {
        memory[index] = other_object.memory[index];
    }
    
    return *this;
}

Now, what happens if we execute the code below:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
Array a(20);

for(int index = 0; index < a.size(); ++index)
{
	a[index] = index;
}

Array c(10);

// 'c' is already created here
c = a;
for(int index = 0; index < c.size(); ++index)
{
	cout << "c[" << index << "] : " << c[index] << endl;
}

Let us see the result:

Array is created with size 20
Array is created with size 10
Array object is being cloned
c[0] : 0
c[1] : 1
c[2] : 2
c[3] : 3
c[4] : 4
c[5] : 5
c[6] : 6
c[7] : 7
c[8] : 8
c[9] : 9
c[10] : 10
c[11] : 11
c[12] : 12
c[13] : 13
c[14] : 14
c[15] : 15
c[16] : 16
c[17] : 17
c[18] : 18
c[19] : 19

See that object ‘c’ is first created just for 10 integers. Copy assignment operator has now reallocated a new memory for storing 20 integers in ‘c’ and then copies all 20 members from other_object, which is here object ‘a’.

One important thing is there to note. In copy assignment operation do not try to copy the same object to itself. We can introduce a self-copy check guard. Use the following check before starting any copy operation, in copy assignment:

‘if(this == &other_object) {}’ , this block will get executed if both objects are same. In this case simply return the same object without any change.

Move semantics in C++ 11: r-value and r-value reference in modern C++

So, what is r-value reference? It is just like any other reference, but here the referred object/data is a temporary value. The best candidates for temporary values are function return values, right hand side of some complex expression which may result in generating temporary objects/data etc. The opposite of r-values are l-values. So from the naming convention they have used here, we can surmise that l-values are those which are present on the left hand side of an expression, and r-values are those present on the right hand side of an expression. Till C++11 there was no support to get r-value references; References could only be made to proper variables/objects/data stored in addressable memory. Actual r-values cannot be referred as their addresses cannot be obtained.

Say,

1
2
int y = 5;
int x = function(y);

Here, it is not possible to get the address of the returned value from function(y) in the expression ‘int x = function(y);’ So this is an example for r-value.

Now, in C++ 11, they have introduced r-value references. For telling the compiler that this data type is an r-value reference just use the syntax ‘&&’. We use a single ‘&’ for declaring l-value references in C++. So for r-value use two &’s i.e. ‘&&’ . Don’t mistake ‘&&’ as reference to reference. There is no such thing in C++. Some of you may think that since there are pointers to pointers in C++, why can’t the same be applied for references. Just treat ‘&&’ as r-value reference and NOT as reference to reference.

Move semantics in modern C++, works only with r-value references. Move means to move from one place to another. Here, it means to move something present inside one object to another object. In operating systems, you must have come across ‘cut’ and ‘paste’ which is analogous to move operation in C++. Yes, so copy means ‘copy’ and ‘paste’, of course. But the point here is, after a ‘cut’ and ‘paste’ operation the original entity (file/text/image) will not be present in its old place and will be moved /shifted to its new location. In C++ move operation, we have to explicitly destroy the content of the original object, but indeed NOT the object itself.

Let us see what happens during move in C++:

See how objects look like before and after move operation. Here a temporary object ‘a’ has been moved to ‘b’, and post move object ‘a”s contents is cleaned to a valid default state.

Some points to note about move semantics:

  1. You cannot move an object with a constant data member. Because you cannot cleanup the constant value post move
  2. You cannot move-assign any objects with references, as references once initialized during object construction, cannot be re-assigned with any values.

Move constructor

Now that, you have understood what move basically means, let us go through the code for implementing move constructor. So here, something is moved from an existing object to a new object just being created. That’s why its move constructor, just like copy constructor.

Let’s see move constructor in action:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// move constructor
Array(Array &&other_object)
{
	cout << "Move constructor: Array has been moved!" << endl;
	size_ = other_object.size_;
	memory = other_object.memory;
	
	//destroy the contents of other_object
	other_object.size_ = 0;
	other_object.memory = nullptr;
}

The interesting syntax of move constructor:

  1. The object is passed as an r-value reference. i.e. ‘Array && other_object’
  2. Second most important thing here is, there is no ‘const’ in the parameter. Of course, the move construction will eventually destroy the contents of other_object then how to declare it as constant.

Let us see how the below code will execute:

1
2
3
4
5
6
Array d(std::move(a)); // std::move converts l-value to r-value.

for(int index = 0; index < d.size(); ++index)
{
	cout << "d[" << index << "] : " << d[index] << endl;
}

Move constructor: Array has been moved!
d[0] : 0
d[1] : 1
d[2] : 2
d[3] : 3
d[4] : 4
d[5] : 5
d[6] : 6
d[7] : 7
d[8] : 8
d[9] : 9
d[10] : 10
d[11] : 11
d[12] : 12
d[13] : 13
d[14] : 14
d[15] : 15
d[16] : 16
d[17] : 17
d[18] : 18
d[19] : 19

Here, object ‘d’ is move-constructed from object ‘a’. Object ‘a’ is not a r-value, but std::move() will convert l-value to r-values. That’s all!

NOTE: std::move() will not do the move operation itself. It will just convert l-values to r-values.

Move assignment operator

Just like ‘copy assignment’ operator, there is its ‘move’ counterpart. Let us see this in code, and you will understand without further explanation.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
// move assignment operator
Array& operator=(Array&& other_object)
{
	cout << "An Array object is moved to this Array object" << endl;
	if(this == &other_object)
	{
		return *this;
	}
	
	size_ = other_object.size_;
	delete []memory; // so important. Release the old memory to heap
	memory = other_object.memory;
	
	// destory the contents of other object
	other_object.size_ = 0;
	other_object.memory = nullptr;
	
	return *this;
}

Make sure to release back the old memory acquired in the moved to object back to heap. See line number 11 in the above code.

Let us see the result of code shown below:

1
2
3
4
5
6
7
Array e(10);
e = std::move(d);

for(int index = 0; index < e.size(); ++index)
{
	cout << "e[" << index << "] : " << e[index] << endl;
}

In the above code, an existing object ‘d’ has been moved to ‘e’. Note that we have used std::move() on ‘d’ to make it an l-value.

Array is created with size 10
An Array object is moved to this Array object
e[0] : 0
e[1] : 1
e[2] : 2
e[3] : 3
e[4] : 4
e[5] : 5
e[6] : 6
e[7] : 7
e[8] : 8
e[9] : 9
e[10] : 10
e[11] : 11
e[12] : 12
e[13] : 13
e[14] : 14
e[15] : 15
e[16] : 16
e[17] : 17
e[18] : 18
e[19] : 19

So that’s it folks, we have covered copy and move semantics in modern C++.

If you want to see the Array class implementation in its full glory and the complete code for testing the copy/move operations, see below:

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
#include <iostream>

using namespace std;

class Array
{
public:
    enum class error_code{bad_size = -1, bad_index = -2};
private:
    int *memory;
    int size_;
public:
    explicit Array(int size)
    {
        if(size <= 0)
        {
            cout << "Bad size" << endl;
            throw error_code::bad_size;
        }
        
        size_= size;
        memory = new int[size_];
        
        cout << "Array is created with size " << size_ << endl;
    }
    
    ~Array()
    {
        cout << "Array is destroyed" << endl;
        
        if(memory != nullptr)
        {
            delete []memory;
        }
        
    }
    
    // copy constructor
    Array(const Array& other_object)
    {
        cout << "Array object's clone is created" << endl;
        size_ = other_object.size_;
        memory = new int[size_];
        
        // start copying byte by byte
        for(int index = 0; index < size_; ++index)
        {
            memory[index] = other_object.memory[index];
        }
    }
    
    // copy assignment operator 
    Array& operator=(const Array& other_object)
    {
        if(this == &other_object)
        {
            return *this;
        }
        
        cout << "Array object is being cloned" << endl;
        
        if(size_ < other_object.size_)
        {
            delete []memory;
            memory = new int[other_object.size_];
        }
        
        size_ = other_object.size_;
        // Copy byte by byte to memory
        for(int index = 0; index < size_; ++index)
        {
            memory[index] = other_object.memory[index];
        }
        
        return *this;
    }
    
    // move constructor
    Array(Array &&other_object)
    {
        cout << "Move constructor: Array has been moved!" << endl;
        size_ = other_object.size_;
        memory = other_object.memory;
        
        //destroy the contents of other_object
        other_object.size_ = 0;
        other_object.memory = nullptr;
    }
    
    // move assignment operator
    Array& operator=(Array&& other_object)
    {
        cout << "An Array object is moved to this Array object" << endl;
        if(this == &other_object)
        {
            return *this;
        }
        
        size_ = other_object.size_;
        delete []memory; // so important. Release the old memory to heap
        memory = other_object.memory;
        
        // destory the contents of other object
        other_object.size_ = 0;
        other_object.memory = nullptr;
        
        return *this;
    }
    
    // indexing operator 
    int& operator[](const int index)
    {
        if(   ( index >= size_ ) 
            ||( index < 0 ) )
        {
            cout << "Bad index" << endl;
            throw error_code::bad_index;
        }
        
        return memory[index];
    }
    
    int size() const
    {
        return size_;
    }
    
};

int main()
{
    Array a(20);
    
    for(int index = 0; index < a.size(); ++index)
    {
        a[index] = index;
    }
    
    // lets copy construct object 'b' from 'a'
    Array b(a);
    
    for(int index = 0; index < b.size(); ++index)
    {
        cout << "b[" << index << "] : " << b[index] << endl;
    }
    
    Array c(10);
    // 'c' is already created here
    c = a;
    
    for(int index = 0; index < c.size(); ++index)
    {
        cout << "c[" << index << "] : " << c[index] << endl;
    }
    
    Array d(std::move(a)); // std::move converts l-value to r-value.
    
    for(int index = 0; index < d.size(); ++index)
    {
        cout << "d[" << index << "] : " << d[index] << endl;
    }
    
    Array e(10);
    e = std::move(d);
    
    for(int index = 0; index < e.size(); ++index)
    {
        cout << "e[" << index << "] : " << e[index] << endl;
    }
    
    
    return 0;
}

Before concluding this session on copy and move, in C++ there are default copy, move constructors and assignment operators. I have already explained the issue of shallow copy with the default copy constructor in this chapter. It is strongly suggested to write your own explicit copy/move constructors and assignment operators for your class if it has some resource management related things as seen in the ‘Array’ class presented in this chapter. Also, one more thing. If you don’t want your class to support copy/move operations, ask the compiler to delete these operations from your class by using ‘=delete’. For example, suppose you have a class My_Class, for which you don’t want to have any copy and move operations. Do this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
class My_Class
{
    My_Class()
    {
        
    }
    ~My_Class()
    {
        
    }
    
    My_Class(const My_Class& other_object) = delete;
    My_Class& operator=(const My_Class& other_object) = delete;
    My_Class(My_Class&& other_object) = delete;
    My_Class& operator=(My_Class&& other_object) = delete;
    
};

On the contrary, if you want the compiler to generate these for you then use ‘=default’ for the required operation. The thing to note here is only the operation you have selected will get generated by the compiler, while others will not be generated at all. i.e if you default generate copy constructor, then copy assignment, move constructor and assignment operations will not be default generated by the compiler.

I have not explained how copy/move semantics should be applied for class hierarchies so far in this chapter. This topic will be discussed in next chapter. For now, let’s concentrate on copy/move for normal classes without any derived class hierarchies.

Enjoyed this chapter. Let me know in the comments below. Thanks! 🙂

0

Leave a Reply