C/C++ Users Journal September, 2005
If you haven't ever had to track down a program failure caused by a bad pointer, then you're not a serious programmer. That's an unfortunate fact of life in our profession. As applications become more complex, their resource management requirements become more complex, and despite our best efforts, our designs often have holes in them, or we apply our designs incorrectly, or we make coding errors. The result is an uninitialized pointer, a resource leak, or a dangling pointer. The hardest part of tracking these things down is that the program usually fails at a point that has no apparent relationship to the point where the error occurred.
There are tools available to help find these things earlier, but this is an area where better initial planning and execution pay off.
There are three broad categories of resource management techniques: manual resource management, semiautomatic resource management, and automatic resource management.
With manual resource management, nothing happens unless you write code to do it. The most familiar example is new and delete; when you need a new object, you create one with new, which allocates memory for the object and initializes the memory with a constructor. When you no longer need that object you destroy it with delete, which cleans up the object with a destructor and releases the memory that was allocated by new. For a simple example, see Listing 1 [1].
With semiautomatic resource management, you write code that triggers a release of resources, but code in your program keeps track of those resources and makes sure that all the appropriate resources are released. Listing 2 is the same program as Listing 1, except that it uses shared_ptr objects to manage list nodes. The shared_ptr objects take care of releasing nodes that are no longer in use. Listing 2 is slightly shorter than Listing 1. In a normal application, the destructor for a node object wouldn't have to write out a message, and the compiler-generated destructor would be sufficient, so the code would be even smaller.
With automatic resource management, your code simply abandons resources that are no longer needed, leaving it up to the runtime support code to figure out what's still in use and what can be recycled. Listing 3 is the same program as Listing 1, except that it assumes that memory will be recycled automatically. The key difference from Listing 2 is that we're back to raw pointers. We don't need shared_ptr objects to clean up resources: The garbage collector will do it for us [2,3].
The resources that an application manages can be system resources such as memory and files (managed through void*s and FILE*s), or they can be application resources (typically managed through pointers to application-specific types). These resources are allocated by calling a function that returns a suitable pointer and they are released by calling another function with that pointer. In between these two calls, the pointer points to the resource; after the resource has been released, the pointer shouldn't be used. Problems arise when a programmer doesn't follow this protocol and uses a pointer variable before its resource has been allocated or after its resource has been released.
Many applications initialize each pointer to NULL on startup and reset it to NULL when its resource has been released. Checking for NULL before using the pointer avoids accidental use of a bad pointer. Marking pointers in this way, though, isn't a perfect solution. It requires rigid discipline to always check whether a pointer is NULL. Worse, if there is more than one copy of a pointer at the time when its resource is destroyed, it's hard to set all of the copies to NULL. The best way to manage this is to only use one pointer to point to the resource. The drawback to this approach, of course, is that it's inflexible: You can't pass the pointer as an argument to a function, but have to write code that relies on the pointer always being in a known location. Further, marking pointers doesn't help much with resource leaks. You still have to remember to release the resource before the program exits. This can be done with a global object whose destructor releases the resource, or if you need more control over the order in which resources are released, through an atexit function, or with std::auto_ptr, as in Listing 4 [4].
That code is obviously rather cumbersome. The global variables have to be shared between multiple translation units, making maintenance harder. And not being able to pass pointers as function arguments makes for rather unnatural code. The template std::auto_ptr helps a bit with the latter problem. You can pass auto_ptr objects as function arguments, provided you understand their ownership rules. Whenever you copy an auto_ptr object you transfer ownership of the resource that it owns to the new auto_ptr object. When the auto_ptr object that owns a resource is destroyed, the resource's destructor is called. For an example of this ownership transfer in action, see Listing 5.
Most of the time, though, transferring ownership leads to trouble. One example is the standard containers. They copy the objects that they hold, and assume that a copy of an object is equivalent to the original object. That's not the case with auto_ptr objectsthe copy owns the resource, and the original does not. What's needed here is a smart pointer whose objects share resource ownership. TR1 introduces the template std::tr1::shared_ptr, which solves this problem as well as several others.
TR1 adds several template classes and template functions to the header <memory>, all of them in the nested namespace std::tr1. I've already mentioned the template class shared_ptr; we'll look at this in much more detail in this column. It has a companion named weak_ptr, which we'll look at in a future column. You can compare shared_ptr objects for equality, and there is an operator< for shared_ptr objects and for weak_ptr objects, so you can use them as keys in associative containers. There are also three template functions that convert shared_ptr types, similar to const_cast, dynamic_cast, and static_cast. For more detail, see Listing 6.
It's easy to get confused in discussions of smart pointers. To try to reduce that confusion, in our discussions of shared_ptr and weak_ptr, I'll use the following terms:
Listing 7 is a synopsis of the template class shared_ptr. It's really far simpler than it looks. There are three basic operations: You can assign control of a resource to a shared_ptr object, you can ask about the pointer that the object holds, and you can ask about its reference count.
The default constructor creates an empty shared_ptr object. It doesn't own any resources, and its reference count is zero.
You assign control of a resource to a shared_ptr object with the constructors, the assignment operators, the member function swap, and the member functions reset. Each of these takes an argument that designates the resource to be controlled. When that argument is a pointer, the shared_ptr object becomes the sole owner of that resourcethe member function use_count returns 1, and the member function unique returns true. When that argument is another shared_ptr object, the resulting object shares ownership with that argument; both will have the same use count, which will be one more than the use count before the function call. Because ownership of the resource is shared, the member function unique will return false for both shared_ptr objects.
When a shared_ptr object is destroyed, its destructor decrements the reference count for the resource it owns. If the count becomes zero it releases the resource. This is ordinarily done by deleting it; however, you can customize this with a deleter. You can see this reference counting in action in Listing 8.
Listing 8 uses constructors to create objects that own their resources. You can also use the reset functions and the assignment operators to assign ownership of a resource. When you do that, the shared_ptr object first decrements the reference count for the resource that it currently owns, and if that count becomes zero it releases the resource. Then it takes ownership of the assigned resource, in the same way as the constructor; see Listing 9.
Next time, I'll continue this examination of shared_ptr by using the controlled resource and looking at pointer types, shared_ptr types, and deleters, among other topics.