Smart Cleanup

Achieve More Reliable Resource Management with Our Custom C++ Classes

Eric Niebler

Code download available at:ResourceManagement.exe(196 KB)

This article assumes you're familiar with C++ Templates and Managed C++

Level of Difficulty123

SUMMARY

Managing resources in C++ is not easy. When you're unsuccessful, your app can leak all kinds of resources including file system handles, database connections, and, of course, memory. Even in garbage-collected languages like Managed C++, resource management is difficult because garbage collection only deals with memory management, not the other resources that cause performance problems.

In this article, the author describes the SmartAny template library he created, which uses a policy-based approach to dynamic resource management. Readers will learn how to use the SmartAny classes and policies to ensure the proper cleanup of their resources, be they files, OS handles, or native and unmanaged objects.

Contents

Overview
Sample Usage
Class Prototypes
The auto_any Class
The shared_any Class
The scoped_any Class
Managed Code
Conclusion

When I was very young, my mother used to joke that she could tell what path I had taken through the house by the debris I left in my wake. She referred to my carelessly discarded items as "The Trail." After many years of coaching, she taught me to clean up after myself and to put things in their proper place when I was done with them. It's good that I learned this lesson early, because these days my career as a C++ programmer depends on my ability not to leave a trail.

Resource management in C++ is hard. If you forget to clean up after yourself, your application will hemorrhage memory, file system handles, and database connections, if it runs at all. Add exceptions to the mix and it can be very hard indeed to avoid leaving a trail. Garbage-collected languages, all the rage these days, promise to sweep all this mess away, but they can leave an untidy trail behind them, too. Garbage collection (GC) helps with memory management, but memory is only one type of resource that needs attention. GC does nothing to help with windows, handles, critical sections, and the like. Quite the contrary, running finalizers nondeterministically on a separate thread can actually make it harder to clean up these resources correctly.

To automate my housekeeping chores, I developed a generic resource management library which I call SmartAny. The SmartAny library takes a back-to-basics approach to resource management. With SmartAny, I can bind a "cleanup" action to any arbitrary resource and have that cleanup action execute deterministically and synchronously when the resource is no longer needed. SmartAny is useful in both native and managed code. In managed code, it complements and integrates seamlessly with the garbage collector so that I can have deterministic finalization when I need it and let the garbage collector handle the rest. With SmartAny, cleaning up after myself is a snap.

Overview

Most people know about smart pointers like std::auto_ptr. The idea behind them is simple—a wrapper object "owns" the pointer and calls delete on it in the destructor. This way, the memory is guaranteed to be cleaned up. Come hell or high water, return statements, or exceptions, the memory will be freed.

The SmartAny library extends this simple idea to any resource type and to any cleanup action. It is a generic and extensible resource management framework. With the classes in SmartAny, you can manage file handles the same way you manage sockets, the same way you manage...—well, you get the idea.

SmartAny contains three helper classes: auto_any, shared_any, and scoped_any. The auto_any class is analogous to std::auto_ptr. It gives your resources strict ownership semantics; that is, only one auto_any can own a resource at a time, and copying an auto_any implies transfer of ownership. The shared_any class is analogous to boost::shared_ptr. It is a reference-counted resource, which gets cleaned up when the reference count drops to zero. It is appropriate for use in standard containers. The scoped_any class is analogous to boost::scoped_ptr. It is like auto_any, except that copy and assignment are disallowed, making it harder to misuse.

These helper classes work seamlessly with both managed and unmanaged types. For managed types, you can choose a policy that calls the destructor deterministically or one that calls the Dispose method instead. You can even write your own policy that does both. Since the SmartAny usage is the same for managed and unmanaged types, it makes it much easier to write and read mixed code. As you read this article, you may want to take a look at https://www.boost.org and some of the libraries provided there. I found the site very useful and can thank the authors for the design of SmartAny, which was strongly influenced by the smart pointers there.

Sample Usage

Enough theory; let's see some code. The code in Figure 1 uses the SmartAny library to manage the lifetime of a Win32® file handle. Notice the type auto_hfile. It is a typedef provided in auto_any.h. An auto_hfile is an auto_any that wraps a HANDLE, uses the close_handle policy, and has invalid_handle_t as its invalid state. I will discuss each of these individually.

Figure 1 Managing Handle Lifetime

#include <windows.h> #include "auto_any.h" // return an auto_hfile by value auto_hfile open_file( char const * szFileName ) { // auto_hfile is a typedef for // auto_any<HANDLE,close_handle,invalid_handle_t> return auto_hfile( CreateFile( szFileName, /*...*/ ) ); } int main() { auto_hfile hFile = open_file( "my_file.txt" ); if ( hFile ) { // File open succeeded, write something to it WriteFile( get( hFile ), /*...*/ ); } // File is closed automatically when hFile is destructed return 0; }

Now note that open_file returns an auto_hfile by value. That's perfectly OK. The ownership of the HANDLE gets passed to the hFile local variable. It works just like a std::auto_ptr.

Also, notice the if statement. This checks to see if the hFile is in a valid state. It isn't comparing the hFile to NULL, but rather to the invalid_value_t template parameter. This means that you can forget about whether you should be comparing to NULL or INVALID_HANDLE_VALUE; if you're using the right auto_any typedef, then the right thing just happens. It's very easy for you to pick the right auto_any typedef because they use an obvious and consistent naming scheme.

Finally, notice the WriteFile statement. The WriteFile API is expecting a HANDLE, not an auto_hfile. Rather than providing an implicit conversion from auto_hfile to HANDLE, there is a get function which takes an auto_any and returns the contained resource. See the sidebar "SmartAnyRationale" for the reasons why there is no implicit conversion operator and why get isn't a member function in the auto_any class.

Class Prototypes

Behind the typedefs, the SmartAny classes have a policy-based template design that makes them flexible and extensible. Figure 2 shows their prototypes. The type T is the contained resource. In the case of auto_hfile, T is HANDLE.

Figure 2 SmartAny Classes

template< typename T, class close_policy, class invalid_value = null_t, int unique = 0 > class auto_any; template< typename T, class close_policy, class invalid_value = null_t, int unique = 0 > class shared_any; template< typename T, class close_policy, class invalid_value = null_t, int unique = 0 > class scoped_any;

The close_policy class cleans up the resource. It is a class that has a static member function named close that takes an object of type T as a parameter. In the case of auto_hfile, the close policy is close_handle, which is defined in Figure 3.

Figure 3 Close Policy

// a generic close policy that uses a ptr to a function template<typename Fn, Fn Pfn> struct close_fun { template<typename T> static void close( T t ) { Pfn( t ); } }; // close_handle calls the CloseHandle() API typedef close_fun<BOOL (__stdcall *)( HANDLE ),CloseHandle> close_handle;

The invalid_value parameter defines the invalid state for the contained resource. It is the state that the wrappers have before they are initialized and after they have been cleaned up. Instances of the invalid_value type are implicitly convertible to type T and evaluate to the invalid value for type T. Often, the invalid state is NULL, 0, or a default constructed object. All of those cases are covered by null_t, which is the default for the invalid_value policy. In the majority of cases, null_t is exactly what you want, so you don't need to worry about it.

SmartAny Rationale

Once you start using SmartAny, you'll begin to notice some things about its design that might strike you as unconventional. Here are some of SmartAny's features and the rationale behind them.

No Implicit Conversions

An auto_hfile doesn't have an implicit conversion to HANDLE, so you cannot use an auto_hfile transparently as if it were a HANDLE. The same is true for the other SmartAny wrappers and their contained resources. This is by design since SmartAny's intended purpose is to make it easier to manage your resources correctly. An implicit conversion would work against this purpose. If the implicit conversion were available, then code like this would be able to compile:

auto_hfile hFile( CreateFile( /*...*/ ) ); /* lots o' code here */ CloseHandle( hFile ); // oops!

Notice that with an implicit conversion, it is very easy to accidentally use an auto_hfile as a parameter to CloseHandle. This is bad because the hFile object thinks it owns the handle and will try to close it again when it goes out of scope. It's generally a bad thing to free your resources twice. It is especially easy to make this mistake when you begin using SmartAny in a legacy code base. For this reason, the implicit conversion is not provided, and therefore you are required to use the get accessor function to retrieve the contained resource.

OK, One Implicit Conversion

If you snoop around in SmartAny's source code, you will notice that the SmartAny wrappers actually do have an implicit conversion, but it is to a strange type called safe_bool. It is this conversion that allows you to test the wrappers for validity in conditional statements. A conversion to bool would accomplish the same thing, so why the "safe_bool"? The problem is that if you allow an implicit conversion to bool, you also allow a conversion to any intrinsic type that bool can be converted to (char, int, and so on), and this can happen when you least expect it.

The solution is to provide an implicit conversion to some obscure pointer type. The type actually used is a pointer to a member function on a dummy struct. It looks like this:

struct dummy_struct { void dummy_memfun() {} }; typedef void (dummy_struct::*safe_bool)(); safe_bool const safe_true = & dummy_struct::dummy_memfun; safe_bool const safe_false = 0;

Now, I define the implicit conversion operator as:

// for use when auto_any appears in a conditional operator safe_bool() const { return valid() ? safe_true : safe_false; }

The pointer type is convertible to bool, so you can use auto_any objects in conditionals, but the pointer type is not convertible to anything else, so you don't get any nasty surprises.

No Named Member Functions

If you've used std::auto_ptr, you know that get, reset, and release are member functions. You may be surprised to discover that the SmartAny wrappers do not have these member functions. Rather, SmartAny provides get, reset, and release as friend functions. Wrappers like std::auto_ptr that have named member functions can lead you astray, as the following code fragment clearly illustrates:

struct mine { void release() { /*...*/ } }; std::auto_ptr< mine > p( new mine ); p->release(); p.release();

Notice the last two lines. At a glance, you might not see the difference. Although they look very similar, they are doing two completely different things. The first is calling mine::release, and the second is calling std::auto_ptr::release. It's very easy to type one when you meant to type the other, or accidentally transpose the two lines, with disastrous results that will only expose themselves at run time.

Had release been implemented as a friend function, the two calls would have to be written as:

p->release(); release( p );

This method is less prone to error since it's just about impossible to type one when you mean to type the other since the syntax is not so similar. As a rule, all named SmartAny "member functions" are actually implemented as friend functions.

No Direct Assignment

There is no assignment operator for assigning a resource directly to a SmartAny wrapper. As with std::auto_ptr, you must use reset to assign a new resource to an existing wrapper. The rationale is that assigning to a smart wrapper is not just a simple copy; it implies a transfer of ownership. In addition, if the wrapper is already managing a resource, that resource will be freed before the assignment takes place. Innocuous-seeming code like "foo = bar" is not at all suggestive of what is really going on. Code like "reset( foo, bar )" states your intention in no uncertain terms. This is a flashing neon sign to future maintainers of your code that screams that it is releasing the resource held by foo and taking ownership of bar.

To smooth over a wrinkle in the Win32 API regarding file handles, auto_hfile uses a different invalid_value policy. Consider the following code snippet:

auto_hfile hFile( CreateFile( /*...*/ ) ); if ( hFile ) { /*...*/ }

Seasoned programmers who use Win32 know that the CreateFile API returns INVALID_HANDLE_VALUE on failure, not NULL. They want the if statement to test the file handle against INVALID_HANDLE_VALUE. Fortunately, auto_hfile accomplishes this by using invalid_handle_t, defined in Figure 4.

Figure 4 invalid_handle_t

// value_const is a general invalid_value policy that evaluates to a // literal constant. template<typename T,T value = T(0)> struct value_const { operator T const() const { return value; } }; typedef value_const<HANDLE,INVALID_HANDLE_VALUE> invalid_handle_t;

When you test an auto_hfile for validity in a conditional, you are really testing against an invalid_handle_t. Code like "if ( hFile )" means the same thing as "if ( get( hFile ) != invalid_handle_t() )", which amounts to "if ( get( hFile ) != INVALID_HANDLE_VALUE )", which is exactly right.

You will usually not have to worry about the last template parameter (unique). Its purpose is to provide an extra level of type safety. There are some resources that the Win32 API treats polymorphically, even though they are very different conceptually. For example, events and threads are both represented by HANDLEs and are cleaned up by calling CloseHandle. Many programs don't need the ability to treat threads and events polymorphically, and in fact it can be dangerous to do so. The SmartAny library employs the unique template parameter to give threads and events strong types, preventing you from using one in situations where the compiler is expecting the other.

The auto_any Class

The auto_any class is modeled after std::auto_ptr. It has very useful but somewhat counterintuitive copy semantics. In particular, only one auto_any is allowed to own a particular resource at a time. When you assign from one auto_any to another, both objects change. One object gives up ownership, and the other accepts it. When I speak of "ownership," I mean the object contains a valid resource. After an auto_any has given up ownership, it no longer contains a valid resource (see Figure 5). This has profound implications. If you pass an auto_any by value to a function, you are giving ownership to the function. Your local auto_any will become invalid (oops!), and the resource will be automatically cleaned up when the function returns (oops!). It's the roach motel of functions—resources go in, but they don't come out. Sometimes this is exactly what you want; often it isn't.

Figure 5 Transferring Ownership

auto_event e1( CreateEvent( /*...*/ ) ); // e1 is valid here assert( e1 ); auto_event e2; // e2 is not valid yet assert( ! e2 ); // transfer ownership! e2 = e1; // now e2 is valid ... assert( e2 ); // ... and e1 is not!!! assert( ! e1 );

Returning auto_anys by value is a far more useful idiom. Such a function allocates a resource, wraps it in an auto_any, and returns it by value, unambiguously passing ownership to the caller. If the caller ignores the return value, the resource is freed automatically, eliminating a whole category of resource leaks.

The nonstandard copy semantics of auto_any make it unfit for the standard containers. If you would like a smart wrapper that is safe to put in a container, shared_any is your best bet.

All auto_any objects have the same memory footprint as the type they contain. So, for example, sizeof( auto_event ) == sizeof(HANDLE ). This is useful because an array of auto_events can be cast to a HANDLE* and passed to an API such as WaitForMultipleObjects.

The shared_any Class

A shared_any object uses reference counting to determine when it should clean up a resource. It has standard copy semantics, so it is safe to put shared_anys into standard containers. When you copy a shared_any, it increments its reference count. When a shared_any goes out of scope, it decrements its reference count. If the reference count goes to zero, the contained resource is deallocated (see Figure 6).

Figure 6 Shared_any Class

Figure 6** Shared_any Class **

A shared_any object does not ordinarily have the same memory footprint as the contained resource. If you have an array of shared_events, you cannot safely cast the array to a HANDLE*. Use auto_any or scoped_any for that. However, if you wrap a COM pointer in a shared_any (for example, if you use the close_release_com policy), then you automatically get intrusive reference counting (see Figure 7). That is, instead of maintaining a separate reference count, shared_any will use COM's reference counting via IUnknown::AddRef and Release. These "COM-ified" shared_anys are more efficient, and they have the same memory footprint as the bare COM pointer. You can assign an auto_any to a shared_any. If you do, the auto_any gives up ownership, and the shared_any takes ownership with a reference count of 1.

Figure 7 Shared_any Object

Figure 7** Shared_any Object **

The scoped_any Class

A scoped_any object is the simplest type of smart wrapper. Like auto_any, it guarantees that only one scoped_any can own a particular resource at a time. Unlike auto_any, it accomplishes this by disallowing copying and assignment altogether. This makes it less flexible than auto_any, but much more difficult to use incorrectly. If all you want is a way to guarantee that a resource will get deallocated whenever a scope is left, scoped_any is a solid choice. You cannot make the mistake of passing a scoped_any to a function by value—the compiler won't let you. Since copy and assignment are disallowed, you can't put a scoped_any into a standard container. Figure 8 will help you choose the right tool for the job.

Figure 8 Choosing a Helper Class

Requirement auto_any shared_any scoped_any
Strict ownership Yes No Yes
Standard copy semantics No Yes No
Pass and return by value Yes Yes No
Safe for standard containers No Yes No
Same footprint as contained type Yes No Yes
Works with managed types Yes Yes Yes

Managed Code

The SmartAny library works equally well with managed types. Each of the three SmartAny classes can contain a managed pointer. You can choose the close_delete policy, which will invoke the destructor, or the close_dispose policy, which will invoke the Dispose method. Or you could write your own to do both. Figure 9 shows an example of using auto_any with a managed object.

Figure 9 Using auto_any with a Managed Object

#using <mscorlib.dll> #include <windows.h> #include "auto_any.h" __gc struct MyObject : System::Object, System::IDisposable { virtual ~MyObject() { System::Console::WriteLine(S"~MyObject!!!"); } virtual void Dispose() { System::Console::WriteLine(S"Dispose!!!"); } void Hello() { System::Console::WriteLine(S"Hello, world!"); } }; int main() { // This object is automatically disposed auto_any<MyObject*,close_dispose> pO1( new MyObject ); // This object is automatically deleted auto_any<MyObject*,close_delete> pO2( new MyObject ); // Just like any smart pointer, this does what you expect pO2->Hello(); return 0; }

As you can see, the syntax for dealing with managed types is exactly the same as dealing with unmanaged types. This lets you have deterministic finalization with your managed objects with code that is familiar and easy to write and maintain.

Conclusion

The SmartAny library comes with a host of useful typedefs, canned cleanup policies, invalid-value policies, and accessor functions. You can download the full source code from the link at the top of this article.

For related articles see:
/msdnmag/issues/1100/gci

For background information see:
Modern C++ Design: Generic Programming and Design Patterns Applied by Andrei Alexandrescu (Addison-Wesley, 2001)

Eric Nieblerstudied Computer Science and Mechanical Engineering at the University of Virginia. He has worked at Microsoft in various capacities since 1996; first, as a tester in the Windows 2000 group, then as a developer in Microsoft Research. These days, he calls the Visual C++ Libraries Team home. He can be reached at ericne@microsoft.com.