Hello again! In this video, we are going to look at shallow and deep copying.
We are building up this String class with a capital "S", which is going to replicate some of the functionality
of the library string class.
So far we have taken the easy option and used a library string member. Now we are going to make it a bit more
realistic.
We are going to provide our own storage.
We need to have an array to store the characters.
And this array is going to be stored on the heap.
So our class will now have a member which is a pointer to char, so that is going to be a pointer to
the memory, an array. We are going to have a size member which has the number of elements in the array,
the number of characters in the data.
And then we have some member functions for the interface.
This is going to be written to use the RAII idiom for managing resources. So it will allocate the memory for
the array in the class's constructor, and the memory will be released in
the class's destructor.
So this is the typical pattern for managing resources in C++.
Here the resource is the allocated memory.
The String class is responsible for managing the lifetime of this resource.
It needs to make sure that the resource is correctly acquired before any member functions in the class
use it.
It needs to make sure that the resource is correctly released when the class no longer requires it.
If there are any objects being copied, it needs to make sure that the copying of the resource is handled
correctly. And, looking ahead to move semantics,
if there is any transfer of the resource from one object to another, then that needs to be handled correctly
as well.
We are going to leave that to a later section.
In this video, we are going to concentrate on the copying.
So here is our String class with these two members, one for the data and one for the number of elements
in the array. In the constructor, we need to allocate the memory.
So we need to know how many elements we need to allocate.
We are passed a standard string as argument.
So if we call the size member function of that string, that will give us the number of elements, then
we can initialize our own size member from that.
And then in the body of the constructor, we can allocate the memory.
In fact, you could actually move this into the initializer list, if you prefer, but I think it is clearer
this way. It will not really make any difference.
So we allocate a memory buffer, which contains "that many" elements, and then we just iterate through
it, and we populate the data with the characters from the argument string.
For the copy constructor and the assignments operator, we're going to use the ones which were synthesized
by the compiler, for the time being.
[In order to] see what that does.
Then we have a destructor where we release the memory.
So we call delete.
Because we used the array form of new in the constructor, we have to use the array form of delete here.
If we missed that out, we will probably crash the program.
I have also put in a print statement so we can see what is going on. So we can see when the destructor is
called.
There is also a length() member function, which will return the value of this size member. So we can
see how many elements there are in the string. Then, in our main function, we create a couple of objects,
by calling the constructor.
Then we create another object, by calling the copy constructor.
We call the length member function.
And then we
finish.
So when the program leaves the main function, that will be the end of the scope in which these String
objects were defined. The destructor for these Strings will be called.
These are going to be destroyed in the reverse order
they were created. So string3 will be destroyed first, then string2, then this string.
So we should see three destructor calls.
Were you expecting that?
So we have a problem.
It does not tell us anything useful about what the exact problem is, but it is something to do with the
heap being invalid or a pointer being invalid.
So.. and also we only see two destructor calls.
So this destructor was called and this destructor was called, but something went wrong after that.
And the problem, in fact, is in this synthesised copy constructor. This has just copied the data
points.
So the pointer in the argument object has overwritten the pointer in the new object.
So they both have the same pointers.
So we have two objects pointing to the same memory.
So when their destructors call delete, the arguments to delete is the same pointer.
So we are trying to release the same memory twice.
And the second time, that will mean that we are releasing memory that does not belong to us.
So we get the memory error.
So what we need to do, is to do a "deep" copy.
We did a "shallow" copy before where we were just copying the pointer addresses.
Now we actually need to do it properly.
So we need to give the new object a new allocation of memory that belongs to it and no one else.
So we allocate memory.
We find out the number of elements from the argument.
And then it is basically the same as the constructor, except instead of having a standard string, we
have one of our own objects as the argument.
And then the rest of the code is the same, so let's see what happens now.
And that is much better.
So we do not get a crash, and we get three destructor calls.
We also have the same problem when we perform an assignment.
The compiler will synthesize an assignment operator, which does a shallow copy.
So the pointer gets overwritten. And then we should get the same result as we did with the shallow
copy constructor.
I have added a couple of extra lines, so we now are doing an assignment and an extra print statement.
So what do you think will happen?
And yes, there we are.
We get the crash again. And again,
we only get two destructor calls.
The rest of it seems to have worked, so we called the copy constructor.
The two Strings have three elements, so that seems to have worked. And that still has one element.
So it was just the destructor which was caused by the shallow copy.
So obviously, we need to do a deep assignment.
That is a little bit more complex than the copy constructor.
So obviously, we need to make a new memory allocation for the target object.
The one that is being assigned to. Before this, we need to release the old allocation, because that is
the last time that we have access to the address of that memory.
If we leave it till later, we will have lost that address.
So we need to release that before we do the allocation.
There is also one important thing we need to do before we do any of this.
We need to check for self assignment.
It is possible if it's the object, which is the argument to the assignment
operator, could be the same object as the one that is the target.
You may say that is a bit pointless.
Nobody ever writes code like "x = x", and usually they do not. At least not directly.
If you have function calls and loops and function calls and more loops and lots of complicated nested code,
it is possible that you have some code that assigns the elements of an array to the elements of another
way, and that the two arrays are actually the same.
And why is this a problem?
If the two objects are the same, and in C++, that means they have the same address.
If the two objects are the same, then the pointers to the data will have the same address. And the pointer
in the target object,
"this", will have the same address as the pointer in the argument object.
If we delete the pointer in "this", then the pointer in the argument object will be invalid.
And then later on, when we try to copy the data from the argument array, that has been released. And we
get a memory violation error.
So that is why we need to check.
And also, if the objects are identical, then we do not need to assign them anyway, because they already
have the correct values.
So if you don't like the argument about crashing, perhaps you might like the argument about optimization!
(If you are a true C++ programming, anything to do with optimization or efficiency will make you prick
up your ears!)
So here is our code again.
It is all the same, except that I have now implemented the assignment operator.
So here it is.
We have a print statement as usual, to help us see when it is being called, and then we check for self
assignment.
So if the two objects have the same addresses, so if the address of the argument object and the address
of "this" object are the same, then we skip all this and we go straight to returning the object.
If they are different, then it is safe to release the original memory allocation.
Then we allocate the new memory allocation.
We said to the size member with the number of elements, and then we do the iteration.
So this is the same as doing the destructor, and then the code from the copy constructor.
And we have the same main function, so let's see what happens now.
So there we are, that looks all right. And we get three destructor calls and no crashes.
So how do we know when we need to implement the copy constructor or the assignment operation?
There is something called the "Rule of Three", which says if the class needs to implement one of these -
copy constructor, assignment operator or destructor, then it probably needs to implement the other
two as well, because the ones that the compiler synthesizes will not give the correct behaviour.
So we have our example, where we needed deep copying for the pointer, and that was the rule of three.
(With an extra quotation mark!)
You may be wondering about the default constructor, because the compiler will also synthesize a default constructor.
Usually, that is not a concern because, if we are managing a resource, then the constructor will take some
argument.
So we need to know how much memory to allocate, the name of the file we are going to open, how to connect
to the database and so on.
And if we implement a constructor which takes an argument, then the compiler will not synthesise
a default constructor.
Sometimes it is useful just to have a default constructor, so you can create an empty object.
And in that case, you can watch one which will initialize the object with an empty state, whatever
that means.
So for the case of our String, that means a String with no data, so the data pointer will be null and
the size member will be 0.
And finally, there is also the "Rule of Zero", which says, if the compiler does synthesize special
member functions which give the correct behavior, then forget about it!
Do not bother implementing them, because that will be fine.
Just concentrate on the rest of your code.
So this rule of three is really only for resource management and classes which need unusual copying,
or assignment, or destruction.
OK, so that is it for this video.
I will see you next time, but until then, keep coding!