A TR1 Tutorial: Smart Pointers

WEBINAR:On-Demand

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.

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.

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.

Class std::tr1::shared_ptr

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.

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.

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).

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.

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.

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.

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()).

A TR1 Tutorial: Smart Pointers

WEBINAR:On-Demand

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.

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.

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.

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.

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.

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.

A TR1 Tutorial: Smart Pointers

WEBINAR:On-Demand

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.

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.

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.

Advertiser Disclosure:
Some of the products that appear on this site are from companies from which QuinStreet receives compensation. This compensation may impact how and where products appear on this site including, for example, the order in which they appear. QuinStreet does not include all companies or all types of products available in the marketplace.

Thanks for your registration, follow us on our social networks to keep up-to-date