C++11 shared_ptr class internal workings with code example


C++11 shared_ptr has proven it’s utility in the field of dynamic memory programming. The smart behavior it exhibit in tracking all the pointers associated with it’s storage and the ultimate freeing of memory when the storage is not own by any pointer does make it worthy of being called a smart pointer.But what is it that makes shared pointer so smart about it? what algorithm does the shared_ptr class implement that makes shared pointer behave the way it is? and how are the member functions written? These are some of the questions which we will explore in this post and also try to reproduce a template of our own that behave like the original shared_ptr class.(If you don’t know what is shared pointer you can visit this link.)

The most important thing we should keep in mind when writing a class template like shared_ptr is 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 member require for class like shared_ptr.


Constructor

The shared_ptr class template require 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).And the function of the constructor is to initialized 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)) ;

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 required 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 work 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

When copy constructor is call or when p1 is assigned to p2 shallow copy is performed. This means p2 does not get a storage of it’s 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,so 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 three pointers: p1 ,p2, p3 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.

reference counting
Reference counting.png

Adding a new pointer increase the reference value so what happen if the pointer stops pointing to the storage.A pointer can stop pointing to the storage either when it point to another storage or when it goes out of scope and call it’s 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,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 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 dangling pointer and this 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 gateway to increment and decrement the reference count value when another object point to the same storage or when it cease to exist.After defining the structure reference_counting and declaring it’s pointer as data member of 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 a 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 provided by C++11,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.So let’s be cool and build all the necessary functions brick by brick until our shared_ptr class is capable of replacing the original shared_ptr class.Next let’s add 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 Destructor.But to make a destructor carry out it’s 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 the destructor will look like this.

///Destructor
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 ;
}
}

Don’t get confuse here over the code provided in the destructor.The if() statement is use for an object that is declared and goes out of scope without *storage pointer pointing to any valid storage.So only rc storage is deleted for such object.The else if()statement is use for an object if the *storage pointer point to a valid storage.The two type of objects where if() and else if() is used are shown below.

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

{
shared_ptrobj1(new int(67)) ;
} ///Destructor is call and else if() code is executed

The object obj has ref_count 0 and when it cease to exist the destructor is called and since “rc->ref_count == 0” is true for obj the rc memory is freed.While obj1 has ref_count as 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 behavior of shared_ptr object acting as pointer.Adding ‘*’ in front of the object name will call this function and provide you with the value the ‘*storage’ pointer points 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 Destructor function which is define 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 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
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=() for our shared_ptr class.

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 copy constructor for shared_ptr class 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 .Other than incrementing ref_count value the remaining code will look exactly the same like you have been writing for other normal classes.

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 limitation on how it’s object value 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 before writing the operator=() function:

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 it’s default constructor and so 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 occur.To 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 storages must be deleted.But if the ref_count value is more then 1 the storages 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 the pointers of other object 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 it’s definition outside the calss.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 discuss 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 it’s 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 operators functions which are not discussed here are left for the readers to develop.If you need any help comment below or mail me .


Related Link

-> C++11 Smart pointer : Shared pointer ; why is it call shared pointers?
 
->Member functions of shared_ptr class: use_count() – unique() – get() – swap( ) – reset( )