Introduction
What are smart pointers? They are a means of handling the problems associated with normal pointers, namely memory management issues like memory leaks, double-deletions, dangling pointers etc. This post gives a simple guide to creating your own smart pointer in C++. As a simple starting example, consider a basic template class which can be used to hold generic data types:
template <class T> class Ptr { public: Ptr(T* d) { data = d;} private: T* data; };
And also consider an example class A which we will use the the smart pointer to hold:
class A { public: A() {} ~A() {} void DoStuff() { std::cout << "Hello"; } };
One often-encountered problem is that of forgetting to delete. Or maybe some exception gets thrown and the function is never given the chance to delete. Either way, the result is a memory leak, as would be the case in the following function since the pointer is never deleted:
void function() { Ptr<A> p( new A() ); // memory leak! }
Note that each time function() is called, memory gets allocated for Ptr
but is never deleted when the function goes out of scope. This kind of memory leak is precisely the kind of thing we wish to avoid. We need some mechanism that takes care of the task of deleting the pointer for us, lest we forget.
Given that pointers do not have destructors, we can make our smart pointer class ‘smart’ by giving it one. Our Ptr
class will not only hold the pointer, but delete it when the destructor is called. We no longer have to worry about deleting Ptr
when it goes out of scope:
template <class T> class Ptr { public: Ptr(T* d) { data = d;} ~Ptr() { delete data; } private: T* data; };
Try running function again, to demonstrate that this actually happens. (You may wish to insert a breakpoint in the Ptr
destructor to verify this.)
void function() { Ptr<A> p( new A() ); // pointer deleted when we go out of scope... }
Creating the interface for the smart pointer
Since a smart pointer is meant to look like and behave like a smart pointer without actually being a pointer, it should support the same kinds of interfaces as pointers such as the dereferencing (‘*
‘) and indirection (‘->
‘) operators. This is what the updated Ptr
class looks like with the overloaded ‘*
‘ and ‘->
‘ operators added:
template <class T> class Ptr { public: Ptr(T* d) { data = d;} ~Ptr() { delete data; } T* operator->() { return data; } T& operator*() { return *data; } private: T* data; };
For example, we can now use the overloaded ‘->
‘ (indirection) operator to access the DoStuff
method in class A
, so that it now looks like a standard pointer operation (without actually being one):
void function() { Ptr<A> p( new A() ); p->DoStuff(); // pointer deleted when we go out of scope... }
Using Reference Counting
There is however, one as yet unanticipated danger of deleting the pointer more than once. Try the following code segment to see what I mean:
void function() { Ptr<A> p( new A() ); p->DoStuff(); Ptr<A> q = p; // Danger! pointer double-deleted... }
In here p
is assigned to q
, and both refer to the same class A
pointer. As soon as we go out of scope the destructor of Ptr
is called twice, the first of which is successful, while the second fails, since p and q are referring to the same pointer. The same thing would happen even if we were using normal pointers.
The way around this is to use reference counters to track of the pointers we have added or deleted, and add it to our smart pointer class. We maintain a pointer to the reference counter class in our smart pointer class and this pointer is shared for all instances of the smart pointer that refers to the same pointer. To implement this we must also maintain an assignment operator and copy constructor in our smart pointer class:
class ReferenceCounter { private: int count; public: void Add() { count++; } // Decrement and release reference count int Release() { return --count; } }; template <class T> class Ptr { public: Ptr() : data( 0 ), ref( 0 ) { ref = new ReferenceCounter(); ref->Add(); } Ptr(T* d) : data(d), ref(0) { ref = new ReferenceCounter(); ref->Add(); } // Copy constructor Ptr(const Ptr<T>& ptr) : data(ptr.data), ref(ptr.ref) { // Copy constructor // Copy the data and reference pointer // and increment the reference count ref->Add(); } // Assignment operator Ptr<T>& operator = (const Ptr<T>& ptr) { // Assignment operator if (this != &ptr) // Avoid self assignment { // Decrement the old reference count // if reference become zero delete the old data if(ref->Release() == 0) { delete data; delete ref; } // Copy the data and reference pointer // and increment the reference count data = ptr.data; ref = ptr.ref; ref->Add(); } return *this; } ~Ptr() { // Only when ref. count reaches 0 delete data if(ref->Release() == 0) { delete data; delete ref; } } T* operator->() { return data; } T& operator*() { return *data; } private: T* data; ReferenceCounter* ref; };
Now lets try it in a situation whereby the destructor is called multiple times:
void function() { Ptr<A> p( new A() ); p->DoStuff(); { Ptr<A> q = p; q->DoStuff(); // Destructor of q called here.. Ptr<A> r; r = p; r->DoStuff(); // Destructor of r called here.. } p->DoStuff(); // Destructor of p will be called here }
This is what happens step-by-step: we create a smart pointer p of type A. The constructor of Ptr
is called, the data is stored, and a new ReferenceCounter pointer is created . The Add method of ReferenceCounter increments the reference count from 0 to 1.
The line Ptr q = p;
creates a new smart pointer q
using the copy constructor and the data is copied and the reference further incremented to 2 .
The line r = p;
assigns the value of p
to q
using the asignment operator. The data is also copied and the reference count further incremented to 3. When r
and q
go out of scope, (when we leave the inner curly brackets) the destructors of the respective objects are called.
The reference count is decremented, but the data is not deleted until the reference count is zero. This happens only when the destructor of p
is called. Hence our data is deleted only when nothing is referring to it.
Here is the complete code listing:
#include <iostream> class A { public: A() {} ~A() {} void DoStuff() { std::cout << "Hello"; } }; class ReferenceCounter { private: int count; public: void Add() { count++; } // Decrement and release reference count int Release() { return --count; } }; template <class T> class Ptr { public: Ptr() : data( 0 ), ref( 0 ) { ref = new ReferenceCounter(); ref->Add(); } Ptr(T* d) : data(d), ref(0) { ref = new ReferenceCounter(); ref->Add(); } // Copy constructor Ptr(const Ptr<T>& ptr) : data(ptr.data), ref(ptr.ref) { // Copy constructor // Copy the data and reference pointer // and increment the reference count ref->Add(); } // Assignment operator Ptr<T>& operator = (const Ptr<T>& ptr) { // Assignment operator if (this != &ptr) // Avoid self assignment { // Decrement the old reference count // if reference become zero delete the old data if(ref->Release() == 0) { delete data; delete ref; } // Copy the data and reference pointer // and increment the reference count data = ptr.data; ref = ptr.ref; ref->Add(); } return *this; } ~Ptr() { // Only when ref. count reaches 0 delete data if(ref->Release() == 0) { delete data; delete ref; } } T* operator->() { return data; } T& operator*() { return *data; } private: T* data; ReferenceCounter* ref; }; void function() { Ptr<A> p( new A() ); p->DoStuff(); { Ptr<A> q = p; q->DoStuff(); // Destructor of q called here.. Ptr<A> r; r = p; r->DoStuff(); // Destructor of r called here.. } p->DoStuff(); // Destructor of p will be called here } int main() { function(); return 0; }