A TR1 Tutorial: Smart Pointers

Until TR1, the only smart pointer available in the standard library was auto_ptr; that presents some major disadvantages because of its exclusive ownership model. To address these issues, the new standard offers two new implementations, shared_ptr and weak_ptr, both defined in header <memory>.

Class std::auto_ptr

As mentioned earlier, auto_ptr is based on an exclusive ownership model; each means that two pointers (of this type) cannot point to the same resource. Copying or assigning makes the resource changing its owner, with the source giving the ownership to the destination.

#include <memory>
#include <iostream>

class foo 
{
public:
   void print() {std::cout << "foo::print" << std::endl;}
};

int main()
{
   std::auto_ptr<foo> ap1(new foo);
   ap1->print();
   std::cout << "ap1 pointer: " << ap1.get() << std::endl;

   std::auto_ptr<foo> ap2(ap1);
   ap2->print();
   std::cout << "ap1 pointer: " << ap1.get() << std::endl;
   std::cout << "ap2 pointer: " << ap2.get() << std::endl;
   
   return 0;
}

The output is:

foo::print
ap1 pointer: 0033A790
foo::print
ap1 pointer: 00000000
ap2 pointer: 0033A790

The exact value of the wrapped pointer (0033A790) is not important. The issue here is that, after creating and initializing object ap2, ap1 gave up the ownership of the resource, and its wrapper pointer became NULL.

The major problems introduced by auto_ptr are:

  • Copying and assigning changes the owner of a resource, modifying not only the destination but also the source, which it not intuitive.
  • It cannot be used in STL containers because the constraint that a container's elements must be copy constructable and assignable does not apply to this class.

What's New in TR1?

Two new smart pointers were added to the standard template library:

  • shared_ptr: Based on a reference counter model, with the counter incremented each time a new shared pointer object points to the resource, and decremented when the object's destructor executes; when the counter gets to 0, the resource is released. This pointer is copy constructable and assignable; this makes it usable in STL containers. Moreover, the shared pointer works with polymorphic types and incomplete types. Its major drawback is the impossibility to detect cyclic dependencies, in which case the resources never get released (for example, a tree with nodes having (shared) pointers to children but also to the parent, in which case the parent and the children are referencing each other, in a cycle). To fix this issue, a second smart pointer was created:
  • weak_ptr: Points to a resource referred by a shared pointer, but does not participate in reference counting. When the counters gets to 0, the resource is released, regardless the number of weak pointers referring it; all these pointers are marked as invalid.

The next example shows a similar implementation to the first example, replacing auto_ptr with shared_ptr.

int main()
{
   std::tr1::shared_ptr<foo> sp1(new foo);
   sp1->print();
   std::cout << "sp1 pointer: " << sp1.get() << std::endl;

   std::tr1::shared_ptr<foo> sp2(sp1);
   sp2->print();
   std::cout << "sp1 pointer: " << sp1.get() << std::endl;
   std::cout << "sp2 pointer: " << sp2.get() << std::endl;

   std::cout << "counter sp1: " << sp1.use_count() << std::endl;
   std::cout << "counter sp2: " << sp2.use_count() << std::endl;
   
   return 0;
}
foo::print
sp1 pointer: 0033A790
foo::print
sp1 pointer: 0033A790
sp2 pointer: 0033A790
counter sp1: 2
counter sp2: 2

As you can see, when sp2 is created, sp1 does not give up the ownership, changing its wrapped pointer to NULL; it only increments the reference counter. When the two shared pointer objects get out of scope, the last one that is destroyed will release the resource.

A TR1 Tutorial: Smart Pointers

Class std::tr1::shared_ptr

A sharted_ptr object has the ownership of an object if:

  • It was constructed with a pointer to that resource
  • It was constructed from a shared_ptr object that owns that resource
  • It was constructed from a weak_ptr object that points to that resource
  • Ownership of that resource was assigned to it, either with shared_ptr::operator= or by calling the member function shared_ptr::reset().

All the shared pointer objects that share the ownership of the same resource also shared a control block, containing the number of shared_ptr objects that own the resource, the number of weak_ptr objects that point to the resource, and the deleter (a function used to release the resource), if it has one. An empty shared_ptr object does not own any resources and has no control block. On the other hand, a shared_ptr that was initialized with a NULL pointer has a control block; this means it is not an empty shared pointer. When the reference counter to a resource becomes 0 (regardless the number of weak pointer still referring the object), the resource is released, either by deleting it or by passing its addressed to the deleter.

Creating a shared_ptr

There are several constructors available for a shared_ptr:

shared_ptr();
template<class Other>
   explicit shared_ptr(Other*);
template<class Other, class D>
   shared_ptr(Other*, D);
shared_ptr(const shared_ptr&);
template<class Other>
   shared_ptr(const shared_ptr<Other>&);
template<class Other>
   shared_ptr(const weak_ptr<Other>&);
template<class Other>
   shared_ptr(const std::auto_ptr<Other>&);

You basically can create a new shared_ptr from:

  • A pointer to any type T (including const T), having the possibility of specifying a deleter for the pointed resource
  • Another shared_ptr object
  • A weak_ptr object
  • An auto_ptr object

The next sample shows a shared_ptr created from an auto_ptr object. The auto pointer gives up the ownership of the resource, resetting its wrapped pointer to NULL.

int main()
{
   std::auto_ptr<foo> ap1(new foo);
   ap1->print();
   std::cout << "ap1 pointer: " << ap1.get() << std::endl;

   std::tr1::shared_ptr<foo> sp1(ap1);
   sp1->print();
   std::cout << "ap1 pointer: " << ap1.get() << std::endl;
   std::cout << "sp1 pointer: " << sp1.get() << std::endl;
   
   return 0;
}

The output is:

foo::print
ap1 pointer: 0033A790
foo::print
ap1 pointer: 00000000
sp1 pointer: 0033A790

I was saying earlier that, when a shared_ptr object is created, you can specify a special function called deleter, used to release the resource. If no such function is provided, the resource is simply deleted by calling operator delete.

Consider, for instance, a case when the creation and deletion of a resource should be logged somewhere. For class foo defined at the beginning at the article I created a helper class, that creates and destroys instances, but also logs these events.

class foo_handler
{
public:
   static foo* alloc()
   {
      foo* f = new foo;
      ::OutputDebugString(_T("a new foo was created\n"));
      return f;
   }

   static void free(foo* f)
   {
      delete f;
      ::OutputDebugString(_T("foo destroyed\n"));
   }
};

Each time a new object is created or destroyed, a message is printed in the output window (for simplicity, you will ignore the copy construction or assignment). Function foo_handler::free can pe provided as a delete to the shared_ptr constructor. As a result, when the resource is deleted a message is printed in the output window (you have to run in debugger to see it).

int main()
{
   std::tr1::shared_ptr<foo> ptr(
      foo_handler::alloc(),
      &foo_handler::free);

   ptr->print();

   return 0;
}

Running in debugger and looking into the output window, you can see:

a new foo was created
foo destroyed

Function get_deleter from header <memory> returns a pointer to the deleter of a shared_ptr, if one was provided, or 0 otherwise. The next sample shows how to get the deleter of the shared pointer created earlier.

typedef void (*deleter)(foo*);

deleter* del = std::tr1::get_deleter<deleter>(ptr);

std::cout << "get_deleter(ptr) != 0 == " << std::boolalpha 
   << (del != 0) << std::endl;

The output is:

get_deleter(ptr) != 0 == true

Operators -> and * and function get

Class shared_ptr overloads operators -> and *, the first returning a pointer to the resource and the second a reference to the value of the resource, so that accessing the internal wrapped pointer is not necessary.

template<class Ty>
    class shared_ptr {
public:
    Ty *get() const;
    Ty& operator*() const;
    Ty *operator->() const;
};

Function get() returns the wrapped pointer to the resource (basically identical to operator-> and available for compatibility with auto_ptr).

   std::tr1::shared_ptr<foo> sp(new foo);
   foo* f = sp.get();

   if(f) f->print();

Conditional operator

Class shared_ptr defines a bool operator that allows shared pointers to be used in boolean expressions. With auto_ptr, that is not possible; you have to use function get() to access the internal pointer and check it against NULL.

void is_empty(std::tr1::shared_ptr<std::string> ptr)
{
   if(ptr)
   {
      std::cout << "not empty" << std::endl;
   }
   else
   {
      std::cout << "is empty" << std::endl;
   }
}

int main()
{
   std::tr1::shared_ptr<std::string> sp1;
   std::tr1::shared_ptr<std::string> sp2(new std::string("demo"));

   is_empty(sp1);
   is_empty(sp2);

   return 0;
}

The output is:

is empty
not empty

Swap and assignment

Method swap() and the function with the same name from header <memory> exchange the content of the shared pointers.

int main()
{
   std::tr1::shared_ptr<std::string> sp1;
   std::tr1::shared_ptr<std::string> sp2(new std::string("demo"));

   is_empty(sp1);
   is_empty(sp2);

   sp1.swap(sp2);

   is_empty(sp1);
   is_empty(sp2);

   return 0;
}

The output is:

is empty
not empty

not empty
is empty

On the other hand, operator= is overloaded so that a shared pointer can be assigned from another shared_ptr or auto_ptr.

template<class Ty>
    class shared_ptr
{
public:
   shared_ptr& operator=(const shared_ptr&);
   template<class Other>
      shared_ptr& operator=(const shared_ptr<Other>&);
   template<class Other>
      shared_ptr& operator=(auto_ptr<Other>&);
}

The next sample shows an example of using operator=.

int main()
{
   std::tr1::shared_ptr<int> sp1(new int(1));
   std::cout << "sp1 = " << *sp1 << std::endl;

   std::tr1::shared_ptr<int> sp2(new int(2));
   std::cout << "sp2 = " << *sp2 << std::endl;

   sp1 = sp2;
   std::cout << "sp1 = " << *sp1 << std::endl;
   
   return 0;
}

Methods unique and use_count

Method use_count() returns the number of references to the shared resource (pointed by the current shared pointer object). Method unique() indicates whether another shared pointed shares the ownership of the same resource or not (basically, it's identical to 1 == use_count()).

int main()
{
   std::tr1::shared_ptr<std::string>
      sp1(new std::string("marius bancila"));

   std::cout << "unique : " << std::boolalpha << sp1.unique()
             << std::endl;
   std::cout << "counter : " << sp1.use_count() << std::endl;

   std::tr1::shared_ptr<std::string> sp2(sp1);

   std::cout << "unique : " << std::boolalpha << sp1.unique()
             << std::endl;
   std::cout << "counter : " << sp1.use_count() << std::endl;

   return 0;
}

The output is:

unique : true
counter : 1

unique : false
counter : 2

Resetting

Function reset() decrements the shared reference counter. It then transforms the shared pointer to an empty shared_ptr.

int main()
{
   // a shared_ptr owns the resouce, counter is 1
   std::tr1::shared_ptr<foo> sp1(new foo);
   std::cout << "counter sp1: " << sp1.use_count() << std::endl;

   // a second shared_ptr owns the resourse, shared counter is 2
   std::tr1::shared_ptr<foo> sp2(sp1);
   std::cout << "counter sp1: " << sp1.use_count() << std::endl;
   std::cout << "counter sp2: " << sp2.use_count() << std::endl;

   // first shared_ptr is reset, the counter decremented
   // and the object becomes empty (no control block anymore)
   sp1.reset();
   std::cout << "counter sp1: " << sp1.use_count() << std::endl;
   std::cout << "counter sp2: " << sp2.use_count() << std::endl;
   
   return 0;
}

The output is:

counter sp1: 1
counter sp1: 2
counter sp2: 2
counter sp1: 0
counter sp2: 1

A TR1 Tutorial: Smart Pointers

shared_ptr in STL containers

Unlike auto_ptr that cannot be used in STL containers, shared_ptr can be used because it is copy constructable and assignable. The following sample shows a vector of shared_ptr to int; a transformation is applied on the elements of the vector, doubling the value of the pointed objects.

std::tr1::shared_ptr<int> double_it(const
   std::tr1::shared_ptr<int>& sp)
{
   *sp *= 2;
   return sp;
}

int main()
{
   std::vector<std::tr1::shared_ptr<int>> numbers;

   numbers.push_back(std::tr1::shared_ptr<int>(new int(1)));
   numbers.push_back(std::tr1::shared_ptr<int>(new int(2)));
   numbers.push_back(std::tr1::shared_ptr<int>(new int(3)));

   std::cout << "initially" << std::endl;
   for(std::vector<std::tr1::shared_ptr<int>>::const_iterator
      it = numbers.begin();
      it != numbers.end();
      ++it)
   {
      std::cout << *(*it) << " (counter = " << (*it).use_count()
                << ")" << std::endl;
   }

   std::transform(numbers.begin(), numbers.end(), numbers.begin(),
                  double_it);

   std::cout << "after transformation" << std::endl;
   for(std::vector<std::tr1::shared_ptr<int>>::const_iterator it =
      numbers.begin();
     it != numbers.end();
     ++it)
   {
      std::cout << *(*it) << " (counter = " << (*it).use_count()
                << ")" << std::endl;
   }

   return 0;
}

Notes for the preceding code:

  • *(*it) or **it means two dereferences: the first to get to the shared_ptr object from the iterator, and the second to get to the int object from the shared_ptr;
  • The program shows the reference counter to show that calling function double_it() does not affect it, even though this function returns a shared_ptr by value.

The output is:

initially
1 (counter = 1)
2 (counter = 1)
3 (counter = 1)
after transformation
2 (counter = 1)
4 (counter = 1)
6 (counter = 1)

shared_ptr with class hierarchies

shared_ptr can work with class hierarchies, so that shared<D> is convertible to shared<B>, where D is a class (or struct) derived from B. The following class hierarchy is used to demonstrate the concept.

class Item
{
   std::string title_;
public:
   Item(const std::string& title): title_(title) {}
   virtual ~Item() {}

   virtual std::string Description() const = 0;
   std::string Title() const {return title_;}
};

class Book : public Item
{
   int pages_;
public:
   Book(const std::string& title, int pages):Item(title),
      pages_(pages) {}

   virtual std::string Description() const {return "Book: " +
                                            Title();}
   int Pages() const {return pages_;}
};

class DVD : public Item
{
   int tracks_;
public:
   DVD(const std::string& title, int tracks):Item(title),
      tracks_(tracks) {}

   virtual std::string Description() const {return "DVD: " +
      Title();}
   int Tracks() const {return tracks_;}
};

Having those classes, I will create a vector of shared_ptr<Item>, but add objects of type Book and DVD.

int main()
{
   std::vector<std::tr1::shared_ptr<Item>> items;

   items.push_back(std::tr1::shared_ptr<Book>
      (new Book("Effective STL", 400)));
   items.push_back(std::tr1::shared_ptr<DVD>
      (new DVD("Left of the Middle", 14)));

   for(std::vector<std::tr1::shared_ptr<Item>>::const_iterator
      it = items.begin();
      it != items.end();
      ++it)
   {
      std::cout << (*it)->Description() << std::endl;
   }
   
   return 0;
}

The output is:

Book: Effective STL
DVD: Left of the Middle

Cast operators

To convert back, from shared_ptr<B> to shared_ptr<D>, where D is a class (or structure) derived from B, you can use the cast function std::tr1::dynamic_pointer_cast.

template<class T, class U>
   shared_ptr<T> dynamic_pointer_cast(const shared_ptr<U>& r);

This function does not throw any exception. If the case can be successfully performed, it returns a shared_ptr<T>that shares the ownership of the resource with the initial object (the reference counter is incremented); otherwise, it returns an empty shared_ptr.

int main()
{
   std::tr1::shared_ptr<Item> spi(new DVD("Left of the Middle",
                                          14));
   std::cout << "spi counter: " << spi.use_count() << std::endl;

   std::tr1::shared_ptr<Book> spb =
      std::tr1::dynamic_pointer_cast<Book>(spi);
   if(spb)
   {
      std::cout << spb->Title() << ", " << spb->Pages()
                << " pages" << std::endl;
   }

   std::tr1::shared_ptr<DVD> spd =
      std::tr1::dynamic_pointer_cast<DVD>(spi);
   if(spd)
   {
      std::cout << spd->Title() << ", " << spd->Tracks()
                << " tracks" << std::endl;
   }

   std::cout << "spi counter: " << spi.use_count() << std::endl;
   std::cout << "spb counter: " << spb.use_count() << std::endl;
   std::cout << "spd counter: " << spd.use_count() << std::endl;
   
   return 0;
}

The output is:

spi counter: 1
Left of the Middle, 14 tracks
spi counter: 2
spb counter: 0
spd counter: 2

A second cast function is std::tr1::static_pointer_cast. It returns an empty shared_ptr if the original object is empty, or a shared_ptr<T> object that owns the resource that is owned by the original object. The expression static_cast<T*>(r.get()) must be valid.

static_cast<T*>(r.get());

This function does not throw and, if successful, increments the reference counter.

template<class T, class U>
   shared_ptr<T> static_pointer_cast(const shared_ptr<U>& r);

In the next sample, a vector holds shared_ptr to void. The first element is statically cast to shared_ptr<char>. The cast is valid as long as the source is not empty, regardless of whether the types are compatible or not.

int main()
{
   std::vector<std::tr1::shared_ptr<void>> items;

   std::tr1::shared_ptr<char> sp1(new char('A'));
   std::tr1::shared_ptr<short> sp2(new short(66));

   std::cout << "after creating the shared pointer"  << std::endl;
   std::cout << "  sp1 counter: " << sp1.use_count() << std::endl;

   items.push_back(sp1);
   items.push_back(sp2);

   std::cout << "after adding to the vector" << std::endl;
   std::cout << "  sp1 counter: " << sp1.use_count() << std::endl;

   std::tr1::shared_ptr<char> spc = 
      std::tr1::static_pointer_cast<char>(*(items.begin()));
   if(spc)
   {
      std::cout << *spc << std::endl;
   }

   std::cout << "after casting" << std::endl;
   std::cout << "  sp1 counter: " << sp1.use_count() << std::endl;
   std::cout << "  spc counter: " << spc.use_count() << std::endl;
   
   return 0;
}

The output is:

after creating the shared pointer
  sp1 counter: 1
after adding to the vector
  sp1 counter: 2
A
after casting
  sp1 counter: 3
  spc counter: 3

If I switch the order of adding the elements to the vector, the program will print letter 'B' instead (its ASCII decimal code is 66).

A third casting function is std::tr1::const_pointer_cast that returns an empty shared_ptr if const_cast<T*>(sp.get()) returns a NULL pointer. Otherwise, it returns a shared_ptr<T> object that owns the same resource as the source.

template<class T, class U>
   shared_ptr<T> const_pointer_cast(const shared_ptr<U>& r);

The function does not throw and, if successful, the reference counter for the resource is incremented.

The following sample yields an error:

std::tr1::shared_ptr<const int> csp(new int(5));
std::cout << *csp << std::endl;
*csp += 10;
error C3892: 'csp' : you cannot assign to a variable that is const

To modify the value of the pointer objectt the const specifier must be removed. This is shown below.

int main()
{
   std::tr1::shared_ptr<const int> csp(new int(5));
   std::cout << "csp counter: " << csp.use_count() << std::endl;

   std::tr1::shared_ptr<int> sp =
      std::tr1::const_pointer_cast<int>(csp);
   *sp += 10;

   std::cout << *csp << std::endl;
   std::cout << *sp << std::endl;

   std::cout << "csp counter: " << csp.use_count() << std::endl;
   std::cout << "sp counter: "  << sp.use_count()  << std::endl;

   return 0;
}

The output is:

csp counter: 1
15
15
csp counter: 2
sp counter: 2

A TR1 Tutorial: Smart Pointers

Class std::tr1::weak_ptr

The major weakness of shared_ptr is that it cannot detect cyclic dependencies. In this case, the reference counter is incremented more than it should actually be, so that the resources are no longer released when the shared pointer objects go out of scope. To fix this problem, a second smart pointer was created, weak_ptr, that points to a resource owned by a shared_ptr but does not affect the reference counter; it is a "weak reference." When the last shared_ptr that owns the resource referred by a weak_ptr, the resource is released and the weak pointer is marked as invalid. To check whether a weak_ptr is valid or not, you can use function expired() that returns true if the pointer was marked as invalid.

Even though function get() (that provides direct access to the wrapped pointer) is available, it's not recommended to use it even in single-threaded applications. The safe alternative is function lock() that returns a shread_ptr sharing the resource pointed by the weak pointer.

void show(const std::tr1::weak_ptr<int>& wp)
{
   std::tr1::shared_ptr<int> sp = wp.lock();
   std::cout << *sp << std::endl;
}

int main()
{
   std::tr1::weak_ptr<int> wp;
   {
      std::tr1::shared_ptr<int> sp(new int(44));
      wp = sp;

      show(wp);
   }

   std::cout << "expired : " << std::boolalpha << wp.expired()
             << std::endl;

   return 0;
}

The output is:

44
expired : true

Example of using shared_ptr and weak_ptr together

I was saying at the beginning of this article that a typical example for a cyclic dependency is a tree implementation when the children have a "reference" to the parent. If only shared_ptr was used, both for references to the children and the parent, the reference counters would be incremented more than necessary and resources would not longer be deleted.

The following sample shows such a tree, but uses a weak_ptr to solve the cyclic dependency.

class Node
{
   std::string value_;
   std::tr1::shared_ptr<Node> left_;
   std::tr1::shared_ptr<Node> right_;
   std::tr1::weak_ptr<Node> parent_;

public:
   Node(const std::string value): value_(value){}

   std::string Value() const {return value_;}
   std::tr1::shared_ptr<Node> Left() const {return left_;}
   std::tr1::shared_ptr<Node> Right() const {return right_;}
   std::tr1::weak_ptr<Node> Parent() const {return parent_;}

   void SetParent(std::tr1::shared_ptr<Node> node)
   {
      parent_.reset();
      parent_ = node;
   }

   void SetLeft(std::tr1::shared_ptr<Node> node)
   {
      left_.reset();
      left_ = node;
   }

   void SetRight(std::tr1::shared_ptr<Node> node)
   {
      right_.reset();
      right_ = node;
   }
};

std::string path(const std::tr1::shared_ptr<Node>& item)
{
   std::tr1::weak_ptr<Node> wparent = item->Parent();
   std::tr1::shared_ptr<Node> sparent = wparent.lock();

   if(sparent)
   {
      return path(sparent) + "\\" + item->Value();
   }

   return item->Value();
}

int main()
{
   std::tr1::shared_ptr<Node> root(new Node("C:"));

   std::tr1::shared_ptr<Node> child1(new Node("dir1"));
   std::tr1::shared_ptr<Node> child2(new Node("dir2"));

   root->SetLeft(child1);
   child1->SetParent(root);

   root->SetRight(child2);
   child2->SetParent(root);

   std::tr1::shared_ptr<Node> child11(new Node("dir11"));

   child1->SetLeft(child11);
   child11->SetParent(child1);

   std::cout << "path: " << path(child11) << std::endl;

   return 0;
}

The output is:

c:\dir1\dir11

Conclusions

If auto_ptr was the only smart pointer available in STL until recently (but inadequately implemented), the new classes, shared_ptr and weak_ptr, are truly smart pointers. shared_ptr is based on reference counting (unlike auto_ptr, which is based on exclusive ownership) and is copy constructable and assignable; these features make it usable in STL containers. It works with polymorphic types and incomplete types, but lacks the possibility of detecting cyclic dependencies. weak_ptr is used to solve this problem, "weakly" referring a resource owned by a shared_ptr, without affecting the reference counter.



About the Author

Marius Bancila

Marius Bancila is a Microsoft MVP for VC++. He works as a software developer for a Norwegian-based company. He is mainly focused on building desktop applications with MFC and VC#. He keeps a blog at www.mariusbancila.ro/blog, focused on Windows programming. He is the co-founder of codexpert.ro, a community for Romanian C++/VC++ programmers.

Comments

  • There are no comments yet. Be the first to comment!

Leave a Comment
  • Your email address will not be published. All fields are required.

Top White Papers and Webcasts

  • With JRebel, developers get to see their code changes immediately, fine-tune their code with incremental changes, debug, explore and deploy their code with ease (both locally and remotely), and ultimately spend more time coding instead of waiting for the dreaded application redeploy to finish. Every time a developer tests a code change it takes minutes to build and deploy the application. JRebel keeps the app server running at all times, so testing is instantaneous and interactive.

  • The exponential growth of data, along with virtualization, is bringing a disruptive level of complexity to your IT infrastructure. Having multiple point solutions for data protection is not the answer, as it adds to the chaos and impedes on your ability to deliver consistent SLAs. Read this white paper to learn how a more holistic view of the infrastructure can help you to unify the data protection schemas by properly evaluating your business needs in order to gain a thorough understanding of the applications …

Most Popular Programming Stories

More for Developers

Latest Developer Headlines

RSS Feeds