C++11 shared pointer class internal workings with code example

In this post we will see how the shared pointer template class works or rather the C++11 shared pointer ptr internal workings.

Link :C++11 shared_ptr

The most important thing we should keep in mind when writing a class template like shared_ptr is the reference counting. It is the heart of shared_ptr class. If you know how reference counting algorithm works the remaining becomes simple. But before we discuss what is reference counting and how to implement it in our class, let us see the structure of constructor and the data members required for a class like shared_ptr.


Constructor

The shared_ptr class template requires only one data member and it should be a pointer of the type the class is declared for(one more data member may be required later on). And the function of the constructor is to initialize the address of the dynamic storage to this pointer data member.

template<class T>class shared_ptr
{
T *storage ;

public:
 explicit shared_ptr(T *t=nullptr) : storage(t) { } //make it public

};

int main( )
{
shared_ptr<int> i(new int(90)) ;

… //your code

return 0 ;
}

The keyword explicit added in front of the constructor will allow the constructor to accept only an explicit type. I have not defined the Destructor function because it does more than merely destroying the object and also we must take into account certain factors when defining the Destructor. After we have discussed all the necessary factors we will define the Destructor. The next topic discussed is reference counting .



Reference counting

Reference counting algorithm revolves around counting the number of pointers that specifically point to one same storage. To make the explanation of reference counting concept simpler let’s introduce a variable known as reference value. The purpose of this reference value is to keep track of the number of pointers that point to the same memory storage. Consider a pointer p1 and it points to certain storage in heap.

shared_ptr< int > p1( new int(90) ) ;

In the code above p1 points to the storage with the value 90. The reference value associated with this storage is 1 because only one pointer: p1, points to that storage. If another pointer say p2 points to this storage either by calling copy constructor or by performing assignment the reference value will increase to 2.

shared_ptr< int > p1( new int(90) ) ;

shared_ptr< int > p2(p1) ; //copy constructor is called
//shared_ptr< int> p2=p1 ; //assigning p1 to p2

p>When copy constructor is called or when p1 is assigned to p2 shallow copy is performed. This means p2 does not get a storage of its own but it simply points to the storage pointed by p1. So now there are two pointers pointing to the same storage with the integer value 90, hence the reference value is incremented to 2.

If we introduce a third pointer say p3 and p1 or p2 is assigned to p3 then the reference value will increase to 3 due to the fact that the three pointers p1, p2 and p3 now point to the same location. The reference value is incremented each time a new pointer point to the storage with the integer value 90. The pictorial representation of the reference counting concept is shown below.

C++11 shared pointer internal workings

Adding a new pointer increases the reference value, so what happens if the pointer stops pointing to the storage. A pointer can stop pointing to the storage either when it points to another storage or when it goes out of scope and call its destructor function.

shared_ptr<int>p1(new int(90)) ;

{
shared_ptr<int> p2(p1) ; //copy constructor is called

shared_ptr<int> p3;
p3=p1 ;

shared_ptr<int>p4(new int(546) ) ;

p2=p4 ; //p2 stops pointing to the storage with int value 90

} //p2 goes out of scope

When the pointer p4 is assigned to p3(line 11), the pointer p2 points to the new storage with the integer value 546. The reference value of the storage with integer value 90 is decremented to 2 because only two pointers p1 and p2 now point to that storage.

If p2 also goes out of scope or cease to exist then only p1 will point to that storage and the reference value will again decrease to 1. At last, when no pointer owns the storage the reference value becomes 0 and the storage is freed. By making sure that the storage is freed only when the reference value becomes 0 we are not giving any chances for the pointer to be left out as a dangling pointer and this method works every time.

The shared_ptr class which we have defined earlier cannot implement the reference counting algorithm yet. We require another structure that will keep track of the reference count value. So let’s add another structure and name it as reference_counting and since it will keep track of reference count value it will consist of data member name ref_count to hold the reference count value. A pointer of this structure is declared as data-member of the shared_ptr class (we prefer the data-member to be a pointer not an object since the storage of the structure will be made in heap and only pointer can access such storage). This pointer will also act as a gateway to increment and decrement the reference count value when another object point to the same storage or when it ceases to exist. After defining the structure reference_counting and declaring it’s pointer as a data member of the shared_ptr class the template class will look like this.

struct reference_counting
{
int ref_count ;

reference_counting(int i=0) : ref_count( 1 ) { }

~reference_counting () { }
};

template<class T>class shared_ptr
{
T *storage ;

reference_counting *rc ;

public:
explicit shared_ptr( ):rc(new reference_counting(0)) { storage=nullptr; } ///act as default constructor

explicit shared_ptr(T *t=nullptr) : storage(t) , rc(1) { }

} ;

Two constructors are defined one is the default constructor and the other is a normal constructor. The default constructor is called when an object is just declared without passing any argument. The normal constructor is called when an argument is passed.

shared_ptr<int> obj ; //default constructor called

shared_pt<int>obj1( new int(98) ) ; //normal constructor called

Although we have added constructors to shared_ptr class it still cannot function as the shared_ptr class, if we try to use it now there will be memory leakage as we have not added Destructor or any other function which will safely destroy the object when it goes out of scope.Next let’s add the destructor, use_count() and operator*() functions in our class.


Adding Destructor , use_count() and operator*( ) function in our shared_ptr class

If an object is created it destruction responsibility falls into the hand of the Destructor. But to make a destructor carry out its duty we cannot simply add “delete storage;” and “delete rc;”, we have to take into account if any other object is pointing to the same storage, that means we have to check if the ref_count is 0 and only then we can safely destroy the object. In short what Destructor must do is check if ref_count==0 and if true destroy the object, else do nothing. Adding this functionality to the destructor will look like this.

//Destructor
template<class T>shared_ptr<T>::~shared_ptr( )
{
//Check if ref_count is 0
if ( rc->ref_count == 0 )
{
 delete rc ;
}
else if ( –rc->ref_count == 0 )
{
 delete storage ;
 delete rc ;
}
}

Don’t get confused here over the code provided in the destructor. The if() statement is used only for object that has been declared and gone out of scope without the ‘*storage’ pointer pointing to any valid storage. So only ‘rc’ storage is deleted for such an object.

The else if()statement is used only for an object if the ‘*storage’ pointer point to any valid storage. The two type of objects where if() and else if() code is used are shown below.

{
shared_ptr<int>obj ;
} //destructor is call here and if( ) code is executed for such object

{
shared_ptr<int> obj1(new int(67)) ;
} //Destructor is call and else if() code is executed for such object

The object ‘obj’ has ref_count 0 and when it ceases to exist the destructor is called and since “rc->ref_count == 0” is true for ‘obj’ the ‘rc’ memory is freed.

While obj1 ref_count is 1, under else if() the ref_count value is decremented which makes it 0 and so ‘rc’ and ‘*storage’ is deleted.Here we are doing nothing but making sure that the ref_count is 0 before destroying the memory.



use_count()

use_count() is a member function of the original shared_ptr template provided by C++11(if you want more info on this function go to this link).This function returns the reference count of the object.How to define use_count() function is shown below.

int use_count() const { return rc->ref_count; }

It is a const function because we don’t want this function to change anything.


operator*()

The ‘operator*’ function is the reason behind the behaviour of shared_ptr object acting as a pointer. Adding ‘*’ in front of the object name will call this function and provide you with the value the ‘*storage’ is pointing to. Defining this function is also simple and is shown below.

T& operator*( ) const { return *storage; }

This function is an inline function so adding “template” before the return type is not required like we did with the Destructor function which is defined outside the class; also known as non-inline member function.

So far we have defined constructor,Destructor,use_count and operator*() functions. Let’s rewrite the shared_ptr class including all these functions as a member function and also try to use the class in our program. So now our class will look something like this.

struct reference_counting
{
 int ref_count ;
 reference_counting(int i=0):ref_count(1) { } 
 //The constructor will initialize ref_count to 1

 ~reference_counting () { }
} ;

template<class T>class shared_ptr
{
 T *storage ;
 reference_counting *rc ;

public:
 explicit shared_ptr( ) : rc(new reference_counting(0)) //act as default constructor
 { storage=nullptr; }

 explcit shared_ptr(T *t=nullptr) : storage(t) , rc(1) { }

 //use_count() function
 int use_count() const { return rc->ref_count ;}

 //operator*()
 T& operator*() const { return *storage; }

 //Destructor
 ~shared_ptr( ) ;
};

//destructor definition
template<class T>shared_ptr<T>::~shared_ptr( )
{
if ( rc->ref_count == 0 )
 {
 delete rc ;
 }
else if ( –rc->ref_count == 0 )
 {
 delete storage ;
 delete rc ;
 }
}

int main( )
{
shared_ptr<int>obj1(new int(78) ) ;

cout<< obj1.use_count() << endl
 << *obj1 << endl ;

 cin.get() ;
 return 0 ;
}

Note*:: do not copy one object to another object or do not try to call copy constructor yet using this class, your compiler will throw you an error because we have not defined copy constructor function. Also do not assign one object to another for the same reason that we have not defined operator=() function yet.

shared_ptr<int> obj1(new int(78)) ;

shared_ptr<int> obj(obj1) ; //error!

shared_ptr<int> obj2;

obj2=obj1 ; //error!!

To make our class support copy constructor call and object assigning we have to add them in our class, so let’s get on with it. The next topic is “adding copy constructor in our shared_ptr class”.


Adding copy constructor in our shared_ptr class

The most essential thing you should remember while writing the copy constructor is to increment the ref_count value of the storage whenever it is called. This is absolutely totally necessary. Calling a copy constructor means one more object will point to the storage of the object passed as argument. It naturally follows that the ref_count value should increase. If you fail to increment it mind you, your program will suffer from memory leakage.

shared_ptr( const shared_ptr<T> &sp )
{
 rc = sp.rc ;
 storage = sp.storage ;

 ++rc->ref_count; //increase the reference count
}

Add this copy constructor function in our shared_ptr class and try running the program below.

int main( )
{
 shared_ptr<string>obj1(new string( “Who doesn’t like Charlie Chaplin?” )) ;

{
shared_ptr<string>obj(obj1) ;

cout<< obj.use_count( ) << endl
 << *obj ;
}

cout<< obj1.use_count( ) << endl
 << *obj1 << endl ;

cin.get( ) ;
return 0 ;
}

With the addition of copy constructor function we have added another functionality to our shared_ptr class. But to make our class behave exactly like the original shared_ptr class we require another important operator function: the operator=() function.

Without this function, our class still has a limitation on how the object can be reassigned whenever the need arises.In the next topic we will discuss how to add operator=() function.



Overloading operator=() function

As stated, to assign one object to another we require the operator=( ) function. For the operator=( ) function to work securely without trying to break any of the functionality provided by the shared_ptr class we must take three things into account:

1 note)We must check if the object on the right side is same as the object on the left side.To put it bluntly check if the address of the passed argument is same as the address of the current object using ‘this‘ keyword.If this is the case simply return “*this” as the return value.

template<class T>shared_ptr<T>& shared_ptr<T>::operator=( const shared_ptr<T> &sp )
{
 if ( &sp == this ) //if the address of itself is passed
 {
  return *this ;
 }
}

2 note)When assigning the object if the left object has 0 ref_count value then we must not forget to delete the storage pointed by *rc pointer.Such case arises when the left object is only declared and the *storage pointer is assigned as nullptr.The case is shown below.

 shared_ptr<int>obj1( new int(67) );

 shared_ptr<int>obj ;

 obj=obj1 ;

The ‘obj’ object when declared call its default constructor and so the storage for reference_counting structure is allocated. This storage must be freed before the *rc pointer of ‘obj’ object is assigned to *rc pointer of ‘obj1’ object. If we forget to delete this storage a memory leakage occurs. In the operator=() function shown above we will add an if() statement to check if the left object ref_count is 0.If true the storage is deleted else nothing is done.

template<class T>shared_ptr<T>& shared_ptr<T>::operator=( const shared_ptr<T> &sp )
{
 if ( &sp == this )
 {
  return *this ;
}

 if(rc->ref_count==0 )
 {
  delete rc ;
 }

 rc=sp.rc;
 storage=sp.storage ;
 ++rc->ref_count( ) ;

 return *this ;
}

3 note)In the third case if the left object pointers: *rc and *storage point to valid storage and has a reference count of 1 then the storage must be deleted. But if the ref_count value is more than 1 the storage must not be deleted. When ref_count>1 it means some other object pointers are also pointing to the same storage and if they are deleted those pointers will no longer point to any valid memory. The case is shown in the code example below.

shared_ptr<int>obj1( new int(89) );
shared_ptr<int>obj( new int(34) ) ;

obj=obj1 ; //obj ref_count=1 so storage deleted

shared_ptr<int>obj2(obj1) , obj3(new int(56) ) ; //ref_count of obj1 and obj2 >1

obj2=obj3 ; //storages of obj2 must not be deleted

The code example implementing the three cases and the complete code of the overloaded operator=() function is shown below.

template<class T>shared_ptr<T>& shared_ptr<T>::operator=( const shared_ptr<T> &sp )
{
 if ( &sp == this ) //if the address of itself is passed
{

 return *this ;
}

if(rc->ref_count==0 )
{
 delete rc ;
}
else if( –rc->ref_count ==0 )
{
 delete rc;
 delete storage;
}

rc=sp.rc;
storage=sp.storage ;
++rc->ref_count( ) ;

return *this ;
}

To add this function in our shared_ptr class first of all declare the function name inside the class i.e add “shared_ptr& operator=(const shared_ptr &sp);” inside the class and you can add its definition outside the class. After adding this function you can try out the program below, it should definitely work.

int main( )
{
shared_ptr<int>obj1(new int(7)) , obj ;

obj=obj1 ;
cout<< *obj << endl ;

{
shared_ptr<int>obj2(obj) ;
cout<< *obj2 << endl
 << obj.use_count() << endl;
}

cout<< obj1.use_count() << endl ;

int i=*obj1 ;

cin.get( ) ;
return 0 ;
}

The remaining post discussed some of the member functions of the shared_ptr class.



Adding get()

The get() function returns an address of the storage pointed by *storage pointer. The function is simple and can be written in one line but do not forget to make it const.

T* get( ) const { return storage; }

Also make this function inline.


Adding swap() member function

swap() function will accept one argument as member function but two arguments as non-member functions.This function will exchange the storage pointed by the pointers of the two object.The function definition is shown below.

//swap() member function
template<class T>void shared_ptr<T>::swap( shared_ptr<T> &sp )
{
reference_counting *temp_rc ;
T *tempStorage ;

temp_rc=sp.rc ;
tempStorage=sp.storage ;

sp.rc=rc;
sp.storage=storage ;

rc=temp_rc ;
storage=tempStorage ;
}

The function is non-inline function, so declare the name of the function inside the class to utilize this function. The definition of non-member swap( ) function won’t be shown here try defining it yourself, it’s your homework.


Adding reset( ) function

The reset() function discussed here is of the type that accept no argument.The reset( ) that accept one or two arguments are left for the reader to explore.The general purpose of reset() function is to reset the pointers.The
word “reset” here can carry three different meanings:

i)If the object pointers does not point to any storage then nothing is done.The storage here refer to memory pointed by *storage pointer.
 
ii)If the object pointer *storage point to valid storage then that storage is deleted on calling the reset() function.
 
iii)If the object has a reference count of more than 1 then the storage is not deleted but its ref_count is decremented. After that, rc will point to new storage created with the ref_value initialized to 0 and the *storage pointer is assigned as nullptr.

The code implementing these three cases is shown below.

template<class T>void shared_ptr<T>::reset( )
{

if( rc->ref_count ==0 )
{
 return ;
}
else if( rc->ref_count==1 )
{
 rc->ref_count=0 ;
 delete storage ;
}
else if( rc->ref_count > 1 )
{
 –rc->ref_count ;

 rc=new reference_counting(0) ;
 storage=nullptr ;
 }

}

The remaining member functions and the operator functions which are not discussed here are left for the readers to explore. If you need any help comment below or mail me .