C++: Smart pointers, part 1

This is the first of several posts I wrote related to smart pointers:

  1. Smart pointers
  2. unique_ptr
  3. More on unique_ptr
  4. shared_ptr
  5. weak_ptr

Memory management in C is too error prone because keeping track of each bunch of bytes allocated and deallocated can be really confusing and stressing.

Although C++ has the same manual memory management than C, it provides us some additional features that let us to do this management easier:

  • When an object is instantiated in the stack (e.g. Object o;); the C++ runtime ensure the destructor of such object is invoked when the object goes out of scope (the end of the enclosing block is reached, a premature ‘return’ is found or an exception is thrown); thus, releasing all memory and resources allocated for such object.
  • (Ab)using the feature of operator overloading, we can create classes that simulate the behavior of the pointers. Such classes are called: Smart pointers.

So, smart pointers are classes (typically implemented as class templates, to make them highly reusable) that wrap a pointer and simulate its same behavior, but implement also some policies to release the objects “automatically”. The “magic” of smart pointers lies in the fact that they are always defined as stack variables, thus, the invocation of their destructor is guaranteed.

Consider this piece of code:

#include <iostream>

using namespace std;

class A
{
  private:
    int val;
  public:
    A(int val) : val(val) { cout << "A ctor" << endl; }
    ~A() { cout << "A dtor" << endl; }
    int get_value() const { return val; }
};

int main()
{
  cout << "Enter a number: " << endl;
  int n;
  cin >> n;
  
  A* a = new A(n);
  if (n == 2)
    return 0;
    
  cout << "Value entered: " << a->get_value() << endl;
  delete a;
  return 0;
}

The code shown above asks the user to enter a number, the user enters it and if the
number is 2, the program finishes, otherwise, the program shows the number entered before exiting. If you look into the code you will realize that if the value entered equals 2, the program finishes WITHOUT calling the destructor of A. Though my example is quite trivial, forgetting to call the destructors when returning in several points in our code is one of the most common sources of memory leaks.

Smart pointers are good candidates in this point; we can write a smart pointer class template that releases the object when the smart pointer gets out of scope. To simulate the behavior of plain-old-pointers, the operator* and operator-> must be overloaded: Look into this piece of code implementing my smart pointer:

template <typename T>
class ptr
{
  private:
    T* pointee;
    
  public:
    explicit ptr(T* pointee) : pointee(pointee) { }
    ~ptr() { delete pointee; }
    
    T* operator->() { return pointee; }
    const T* operator->() const { return pointee; }
    
    T& operator*() { return *pointee; }
    const T& operator*() const { return *pointee; }
};

And now look into a modified main() function that uses the smart pointer instead of the normal one:

int main()
{
  cout << "Enter a number: " << endl;
  int n;
  cin >> n;
  
  ptr<A> a(new A(n));
  if (n == 2)
    return 0;
    
  cout << "Value entered: " << a->get_value() << endl;
  return 0;
}

My variable a is defined as a smart pointer and will hold the pointer to a new instance of A() allocated in the heap. As you can see, since the ptr<a> a declaration is a stack declaration, the C++ runtime guarantees that when the main() method will be finished (through the premature return of through the final one), the a destructor will be invoked and it will call the destructor of the pointee object.

A very similar policy is also implemented in the deprecated std::auto_ptr<T> and in the C++11 std::unique_ptr<T> smart pointers.

Other approach of smart pointers is implementing a policy that will keep the track of how many smart pointers are pointing to the same object in the heap. Using reference counting, this kind of smart pointers call the destructor of the objects when the reference count has reached 0 (meaning that there are zero smart pointers pointing to such object in the heap).

A reference counter smart pointer can be implemented in a way similar to this one:

struct refcntptrdata
{
  T* pointee;
  int refcnt;
};

template <typename T>
class refcntptr
{
  private:
    refcntptrdata<T>* data;
    
    void release()
    {
      data->refcnt--;
      if (data->refcnt == 0)
      {
        delete data->pointee;
        delete data;
      }
    }
    
  public:
    explicit refcntptr(T* pointee) : data(new refcntptrdata<T>())
    {
      data->pointee = pointee;
      data->refcnt = 1;
    }
    
    refcntptr(const refcntptr<T>& source) : data(source.data)
    {
      data->refcnt++;
    }
    
    refcntptr<T>& operator=(const refcntptr<T>& source)
    {
      release();
      data = source.data;
      data->refcnt++;
    }
    
    ~refcntptr()
    {
      release();
    }
    
    T* operator->() { return data->pointee; }
    const T* operator->() const { return data->pointee; }
    
    T& operator*() { return *(data->pointee); }
    const T& operator*() const { return *(data->pointee); }  
};

As you can see in my implementation, the refcntptr<T> object just points to a refcntptrdata<T> object that is the one that keeps the pointer to the pointee and the reference counter.

The nice thing on this kind of pointers is that the copy constructor is very cheap because it just increments the reference counter.

C++11 has also a smart pointer implementing this policy: std::shared_ptr<T>

See how this smart pointer can be used in the example below:

int main()
{
  refcntptr<A> a(new A(2));
  refcntptr<A> b(new A(12));
  refcntptr<A> c = a;
  refcntptr<A> d = b;
  refcntptr<A> e(new A(5));
  e = b; //A=5 should be destroyed here because no one is using it anymore
  b = a;
  
  cout << "A: " << a->get_value() << endl;
  cout << "B: " << b->get_value() << endl;
  cout << "C: " << c->get_value() << endl;
  cout << "D: " << d->get_value() << endl;
  cout << "E: " << (*e).get_value() << endl;

}

To read more about smart pointers, visit these posts:

Smart Pointers, part 2: unique_ptr

Advertisement

2 thoughts on “C++: Smart pointers, part 1

  1. Thanks for your article. I would just like to point out a very small error. In the first bullet point you write

    “When an object is instantiated in the stack (e.g. Object o;); the C++ runtime ensure the destructor of such object is invoked when the object goes out of scope (the end of the method is reached, a premature ‘return’ is found or an exception is thrown);”

    Yes, the local object’s destructor is called “when the object goes out of scope .” This is correct.
    But your parenthesis is not quite right: “(The end of the method is reached … ”
    This should be “The end of the enclosing block, is reached”

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s