Hello again!
In this video, we are going to look at reference counting.
Reference counting is a technique which allows different objects, usually of the same class, to access
a shared resource.
We use an integer to keep track of the number of objects which are sharing this resource.
And that integer is known as a reference counter.
Typically, the way this works is that the counter is zero, initially. Because there are no objects which
are sharing the resource.
If we get an object which uses the resource, so we have an object which is bound to this resource, then
we have one object using it.
So the count is now one.
We increment it.
If we add another object, then we now have two objects which are sharing the resource.
So the counter is incremented again and becomes two.
If an object stops using the resource, it is unbound.
Then the counter is decremented.
So we would go down, say, from having two objects, to having one object using the resource.
And if all the objects are unbound, the counter will go down to zero.
And that indicates that the resource is no longer being shared.
We have this string class that we have been using through the course.
We have this data member which is allocated memory, and now we are going to share that, and use reference
counting to manage that.
So we need to have an integer which is going to store the count, the number of objects which are sharing
this memory.
This integer has to be visible to all the objects which are sharing that memory, which are bound to
it.
And the usual way to do that, is to allocate the integer on the heap.
So we are going to add a member to our class, which is going to be a pointer to the reference
counter.
The constructor for our class is going to allocate this counter, as well as allocating the memory.
Initially the counter will have the value zero, because there are no objects which are bound to
the memory.
Then we allocate the memory.
So now we are bound to the memory.
So we need to increment the counter.
So there is now one object using it,
so the counter has a value of one.
So the constructor will look like this.
We allocate the counter, and initialize it to zero.
Then we allocates the memory. And now we have one object which is using the memory, which is us.
So we increment the counter.
For the destructor,
we need to think about what happens when an object of this class is destroyed.
If it is destroyed, then it is no longer bound to the shared
memory.
So we need to decrement the counter.
If the counter is not severe, then that means there are other objects which are still using the memory,
and we must not release the memory.
If the counter is zero, then we are the last object which was bound to that memory.
And now when we go, there will be no-one using it.
So it is up to us to release the memory, and also release the counter, because that is no longer needed.
So the destructor will look like this.
We decrement the counter. And then, if the counter is zero and we are "the last man standing", we need
to release the memory and release the counter.
For the copy constructor, we are initializing a new object to have the same values as the argument.
So these strings s2, and s1 should have the same memory allocation and the same counter.
So we now have another object which is bound to this memory.
So the pointers need to have the same value, so we can just copy those.
Shallow copy is good enough. But we need to increment the reference counter, because we now have another
object.
So the copy constructor will look like this.
We copy the pointer members, and then we increment the counter.
The assignment operator is a bit more complicated.
We can do a shallow copy agenda and increment the counter.
And if the two objects are sharing the same memory, then that is sufficient.
If the two objects are bound to different memory, then the assigned-to objects is going to be unbound
from its old memory, and bound to the new one.
So we need to decrement the counter. And if that brings the counter down to zero, then we need to
release the old shared memory.
So this will be the same code as was in the destructor.
So that is another example of the copy constructed logic consisting of the destructor's logic, followed
by the constructor's logic. And we increment the counter.
But we need to check first that we are not assigning to ourselves.
Otherwise, we will have the counter off by one.
So the full version of the assignment operator looks like this. We check whether the shared memory
is the same.
If it is different, then we need to have the same code as the destructor, to unbind
The object that is being assigned to.
Then we make a shallow copy of the members. And then we increment the counter, provided we are not doing
a self-assignment.
And finally, we return the assigned-to object.
Or alternatively, we can use the copy and swap idiom, which is much simpler.
We create a temporary object.
We swap the members, and then we return the assigned-to object, which will destroy the temporary
object,
when this returns.
The only thing we need to do for this is to add the counter member to the swap() function.
So let's try this out.
So we have the string function.
We have the extra member for the counter. In the constructor,
we allocate the counter and the memory, and then we increment the counter.
I have put some debugging code in here, so we can see what is happening. In the destructor,
we decrement the counter. And, if we are the last man standing, then we release the shared memory and
the counter.
In the copy constructor, we make a shallow copy and increment the counter. In the assignment operator,
we check with the shared objects the same.
If they are different, we unbind the assigned-to object, and then we copy the members.
If we are not doing a self-assignment, then we increment the counter.
Or we could use copy and swap.
And the rest of the code is the same,
more or less. I have added the counter to the print statement.
And then in the main() function we create an object, copy construct, and then assign.
So when we run this "a" and "b" are separate objects, with separate memory allocations.
So they have different memory, and the counters are one.
Each one of these is the only object that is using that memory.
If we copy construct a new object "c" from "b", then "c" will have the same values as "b", they are both sharing
the same memory.
So now we have two objects, and the count is now two.
If we assign "a" from "c", this is the old values of "a". a's memory is difference from "c", so we need to unbind
"a". "a" was the last object which was using that memory.
So we need to release it. And then we give "a" the same values that "c" had.
So now we have "a", "b" and "c" which are all sharing the same memory.
So the count is three.
Then, when we exit the program, the destructors are called for "a", "b" and "c". c's destructor has the count
to three, which it decrements to two. b's destructor decrements the count down to one, a's destructor
decrements the count from 1 to 0, and then there are no objects which are sharing this memory.
So a's destructor will release the shared memory and the counter.
For move operators.
Again, we have a situation where the existing approaches are almost but not quite sufficient. For the
move constructor,
we also need to add a shallow copy of the counter pointer, to the shallow copy of the shared memory.
We need to set the counter to null in the moved-from object,
as well as the data pointer, because all the contents of the object have been moved out of it.
Now that we have set the counter to null, we have code all over the place, which is dereferencing the counter.
So now we need to put in checks, to make sure that the counter is not null before we dereference it.
In the move operations, we do not decrement the counter, because a move operation does not change
the number of bound objects.
All it does, is move the binding from one object to another.
So the count remains the same.
So here is the code that we had before, with the move operators added.
So there is the move constructor, where we do a shallow copy of all the dat,a and then we set the pointers
to null, in the object which is being moved from. In the move assignment
operator, we just do copy and swap.
And then, in the main() function, we move instead of copying.
So again, we have the constructors, which create objects with different memory allocations.
If we move from "b" into the new object "c", "c" has the values that used to be in "b", and "b" now has
null, because all the contents have been moved out.
"c" is now the only object which is using this memory allocation.
So its count is one.
Or if you like, the count has been moved, from "b", into "c".
If we do move assignment, we call the move constructor to create the temporary object, and then, when this
returns, the temporary object is destroyed.
So there has
the data that used to be in "a".
Then "a" has the data that used to be in "c". Up here. And the data that was in "c" is now empty.
So now "a" is the only object which is sharing this memory.
When the destructors are called, "c" and "b" have null pointers, so nothing happens.
The destructor for "a" has a count of one, which it decrements to zero, and then "a" will release the shared
memory and the counter.
Okay.
So that is it for this video.
I will see you next time.
But until then, keep coding!