Strong Pointers and Resource Management in C++
- Resources and Their Ownership
- Smart Pointers
- Strong Vectors
- Code Inspection
- Shared Ownership
- Converting Legacy Code to Resource Management
- Bibliography
Resources and Their Ownership
My favorite definition of a resource is "anything that your program has to acquire and then release." Memory is of course the prominent example of a resource. It's acquired using new and released using delete. But there are many other types of resources—file handles, critical sections, GDI resources in Windows, etc. It's convenient to generalize the notion of a resource to encompass all objects created and released in a program, the ones allocated on the heap as well as the ones declared on the stack or in the global scope.
The owner of a given resource is an object or a piece of code that's responsible for the resource's release. Ownership falls into two classes—automatic and explicit. An object is owned automatically if its release is guaranteed by the mechanisms of the language. For instance, an object embedded inside another object is guaranteed to be destroyed when the outer object is destroyed. The outer object is thus considered to be the owner of the embedded object.
Similarly, every object that's declared on the stack (as an automatic variable) is guaranteed to be released (destroyed) when the flow of control leaves the scope in which the object is defined. In this case, the scope itself is considered the owner of the object. Notice that automatic ownership is compatible with all other mechanisms of the language, including exceptions. It doesn't matter how you exit a scope—normal flow of control, a break statement, a return, a goto, or a throw—automatic resources are always cleaned up.
So far, so good! The problem starts with pointers, handles, and abstract states. If access to a resource is through a pointer, as is the case with objects allocated on the heap, C++ doesn't automatically take care of its release. Instead, the programmer has to explicitly release the resource, using the appropriate programming construct. For instance, if the object in question was created by calling new, it should be deallocated by calling delete. A file that was opened using CreateFile (Win32 API) should be closed using CloseHandle. A critical section entered using EnterCriticalSection should be exited using LeaveCriticalSection, etc. A "naked" pointer, file handle, or a state of a critical section has no owner that would guarantee its eventual release. The basic premise of resource management is to make sure that every resource has its owner.
The First Rule of Acquisition
A pointer, a handle, a state of a critical section will have its owner only if we encapsulate it in objects that follow the First Rule of Acquisition: Allocate resources in constructors and release them in corresponding destructors.
Once you encapsulate all resources according to this rule, you're guaranteed not to have any resource leaks in your program. This is pretty obvious if you only consider those encapsulating objects that are allocated on the stack or are embedded inside other objects. But what about those that are allocated dynamically? Not to worry! Anything that's allocated dynamically is considered a resource, and consequently will also have to be encapsulated according to the rule above. This chain of objects encapsulating objects encapsulating resources has to end somewhere. It ends with the top-level owners, which are either automatic or static. These are guaranteed to be released on exiting the scope or the program, respectively.
Here's a classic example of resource encapsulation. In a multithreaded application, the problem of sharing an object between threads is usually solved by associating a critical section with such an object. Every client that wants to access this shared resource has to first acquire the critical section. For instance, this is how a critical section might be implemented in Win32:
class CritSect { friend class Lock; public: CritSect () { InitializeCriticalSection (&_critSection); } ~CritSect () { DeleteCriticalSection (&_critSection); } private void Acquire () { EnterCriticalSection (&_critSection); } void Release () { LeaveCriticalSection (&_critSection); } CRITICAL_SECTION _critSection; };
The tricky part is that we have to make sure that each client entering the critical section also exits it. The "entered" state of a critical section is therefore a resource and should be encapsulated. The encapsulator is traditionally called a lock:
class Lock { public: Lock (CritSect& critSect) : _critSect (critSect) { _critSect.Acquire (); } ~Lock () { _critSect.Release (); } private CritSect & _critSect; };
Locks are used in the following manner:
void Shared::Act () throw (char *) { Lock lock (_critSect); // perform action -- may throw // automatic destructor of lock }
Notice that, no matter what happens, the critical section is guaranteed to be released by the mechanisms of the language.
There's one important thing to remember—each resource has to be encapsulated separately. That's because resource allocation is almost always a failure-prone operation, if only because there's always a finite supply of any given resource. We'll assume that a failed resource allocation might result in an exception—in fact, very often this is exactly what should happen. So if you're trying to kill two birds with one stone, or allocate two resources in one constructor, you might get into trouble. Just think what happens when the first allocation succeeds and the second one throws. Since the construction hasn't been completed, the destructor won't be called, and the first resource will leak out.
This situation can be avoided easily. Whenever you have a class that requires more than one resource, write small encapsulators for them and embed them in your class. Every fully constructed embedding is guaranteed to be deleted, even if the construction of the embedding object doesn't complete.