In other words, how does the implementation keeps track of the count?
Is there a map-like object maintained which is accessible by all the shared_ptr
in
As far as I remember, there was the problem of reference counting pointer treated in a chapter of Effective C++.
In principle, you have the "light" pointer class, containing a pointer to a class holding the reference which knows to increment/decrement reference and destroy the pointer object. That reference counting class points to the object to be referenced.
Each smart pointer object contains a shared reference count - one for every raw pointer.
You could take a look at this article. This implementation stores these in a separate object which is copied around. You could also take a look at boost's documentation or take a look at the Wikipedia article on smart pointers.
I've seen two different non-intrusive approaches to this:
If you go here and scroll to the bottom, there is an excellent diagram which explains these methods much more clearly.
Creating a memory leak with reference-counting smart pointers is very easy. Just create any graph-like structure of objects that has a cycle in the graph. The objects in the cycle will prevent each other from being released. This can't be resolved automatically - for example, when you create a double-link list you have to take care of never removing more than one object at a time.
No. shared_ptr just keep one additional pointer for reference counting.
When you make copy of shared_ptr object it copy pointer with count of references, increase it, and copy pointer on contained object.
The class that implements RC basically keeps count of number of references (from other objects of the class, including its own) to the memory address that it is managing. The memory is freed only when the reference count to the memory address is zero.
Let’s look at some code:
template <class T>
class SharedPtr
{
T* m_ptr;
unsigned int* r_count;
public:
//Default Constructor
SharedPtr(T* ptr) :m_ptr{ ptr }, r_count{ ptr ? new unsigned int : nullptr }
{
if (r_count)
{
*r_count = 1;
}
}
//Copy Constructor
SharedPtr(SharedPtr& ptr) :m_ptr{ ptr.m_ptr }, r_count{ ptr.m_ptr ? new unsigned int : nullptr }
{
if (ptr.r_count)
{
++(*ptr.r_count);
r_count = ptr.r_count;
m_ptr = ptr.m_ptr;
}
}
//Copy Assignment
SharedPtr& operator=(SharedPtr& ptr)
{
if (&ptr == this)
return *this;
if (ptr.r_count)
{
delete m_ptr;
++(*ptr.r_count);
r_count = ptr.r_count;
m_ptr = ptr.m_ptr;
}
return *this;
}
//Destructor
~SharedPtr()
{
if (r_count)
{
--(*r_count);
if (!(*r_count))
{
delete m_ptr;
delete r_count;
}
}
}
};
Here’s the detail of how the SharedPtr
class above works:
Internal Variables
The internal pointer m_ptr
A pointer of the SharedPtr
class, which is the actual pointer used to manage the memory in question. This pointer variable is shared across multiple SharedPtr
objects, which is why we need a reference counting system to keep track of how many SharedPtr
objects are managing the memory pointed to by this pointer at any point of time during a program’s lifetime.
The reference counter r_count
This is a pointer to an integer type variable, which is also shared across multiple SharedPtr
objects managing the same memory. This is shared because, every SharedPtr
object managing the memory should be aware of the count of every other SharedPtr
object that is managing the same memory. The way to achieve this is by having a common reference counter referred to by SharedPtr
objects of the same family.
Every time a new SharedPtr
object is materialized to manage a memory already being managed by other SharedPtr
object/s, the r_count goes up by 1. It is also decremented by 1 when a SharedPtr
object dies, so that other SharedPtr
objects ‘know’ that one of their family members who was managing the memory maintained by the family has died and no-longer managing the memory.
Default Constructor
When a new SharedPtr
object is created and initialized by a heap allocated memory, this constructor is called where the internal pointer m_ptr
is initialized to the heap allocated memory address that needs managing. Since this is the first and the only reference to that pointer, the reference counter r_count
is set to 1. Nothing interesting happens here.
Copy Constructor and Copy Assignment
This is where the ‘real’ reference counting happens.
Whenever a new SharedPtr
object is made using another SharedPtr
object or an existing SharedPtr
is made to reference another SharedPtr
i.e basically when a new SharedPtr
object (either existing or newly created) is made to manage a memory that was already being managed by other SharedPtr
object/s, the the internal pointer variable m_ptr
of this new manager is made to point at the memory address to be managed and the reference count of the family goes up by 1.
Destructor
Smart Pointers are designed to free the memory they’re managing when they die. In the case of SharedPtr
, it ensures that there are no other references to the memory being managed before freeing the memory. All of these happen in the object’s Destructor.
As you can see in the code, the object frees the memory only if the reference count to the memory is 0, before it dies.
This is important because, you see, if a SharedPtr
object frees the memory when r_count isn’t 0, other SharedPtr
objects managing the same memory would try to access it sometime after and the result would be a program crash.
The SharedPtr
ensures this does not happen by giving the responsibility of freeing memory to the last surviving object that is managing a memory. Due to the design of the SharedPtr
, all of this happens automatically without the programmer’s intervention.
This is how reference counting works.
Reference counting is like the routine of couple of roommates: who leaves the room last has the responsibility of locking the main door. For that to happen seamlessly, every roommate should be aware if he’s the last one to leave the room.