Perform resource recovery

The great majority of exception-handling concerns center on recovering resources when your function is terminated due to an exception. Most handlers just do resource recovery and then pass the exception on.

Automatic objects

The easiest way to handle resource recovery is to tie it to automatic objects (stack-allocated, of storage class auto). An automatic object lives within the scope of the function, and its destructor is called if it still exists when the function is terminated. By tying your resource allocation to the lifetime of an automatic object, you do not have to explicitly catch exceptions.

For example, TMonitorEntry, which is used to acquire a monitor lock, can be allocated on the stack within the scope of the lock. If you exit the scope because of an exception, the lock is automatically released:

      
    
    
      void aFunction() {
      //...
          {
              TMonitorEntry anEntry(myMonitor);
              //... do stuff ...
          }
      //...
      }
Automatic cleanup also applies to base classes and members of an object under construction. If the constructor encounters an exception, it calls the destructors for those base classes and members that have already had their constructors called. The body of your constructor never executes if a base class or data member throws an exception from within its constructor.

NOTE The ISO/ANSI draft specification is currently silent on what happens if an exception occurs during the execution of the constructor in newTFoo;. According to the constructor rule, any base or member objects with completed constructors will have their destructors called. But there is a storage leak, because no pointer is returned (evaluation of the expression is terminated). The prevailing opinion is that operator delete should be called automatically in such situations because there is no other way to completely recover. Taligent expects ISO/ANSI to adopt this specification; the Taligent C++ Compiler is implemented this way.

      void f() {
          TFoo *t = 0;
          try {
              t = new TFoo;
          } catch (...) {
              // t == 0 still
          }
      }

Also consider the TPrimitiveTypeArray template class from the Collection classes. Unlike a variable length C array that you heap-allocate yourself, there's no need to free the array storage if the TPrimitiveTypeArray is on the stack because the destructor cleans it up automatically.

NOTE Heap storage isn't the only resource that must be released when there is an exception. The rest of this section goes into greater detail, especially the discussion of TJanitor on page 108.

Stroustrup also discusses cleanup using automatic variables in xa4 9.4 of The C++ Programming Language. This is the easiest method if you can cast your resource allocation and deallocation in this form. Otherwise, you must explicitly handle exceptions; the remainder of this section discusses a few guidelines.

Passing exceptions

Most handlers just do resource recovery and then pass the exception on. In keeping with that observation, such handlers should look like the following. (For information on when to try to recover from an exception, see "When to recover an exception" on page 112.)

      catch (...) {
          ... do your cleanup ...
          throw;
      }
Place your handlers where you allocate resources in your function. One style is to have many little handlers with cleanup code specific to the resources in that allocated scope:

    void Foo() { TBar *p1, *p2, *p3; p1 = new TBar; try { p2 = new TBar; ... try { p3 = new TBar; ... } catch (...) { delete p2; throw; } } catch (...) { delete p1; throw; } delete p1; delete p2; delete p3; }
As you can see, the previous example is rather messy. Following is a better organizing technique that takes advantage of the fact that you can delete a nil pointer (see "Use nil pointer deletion" on page 135 for more information):

    void Foo() { TBar *p1=NIL, *p2=NIL, *p3=NIL; try { p1 = new TBar; p2 = new TBar; p3 = new TBar; ... } catch (...) { delete p1; delete p2; delete p3; throw; } delete p1; delete p2; delete p3; }
You still have to include the normal deletes of p1, p2, and p3. There's no good way to avoid this problem, unless you don't need access to local variables, in which case you can call a common function to handle it.

Even if your cleanup doesn't involve deleting a pointer, you can use a similar technique with your own flag variables to indicate something needs cleaning up. Although the previous example comes out better with such flags, sometimes nested try blocks are more appropriate (though deep nesting is hard to read and understand); one size does not fit all.

TJanitor

This simple example illustrates an even easier way to handle the storage allocation by using objects. If you have a helper class declared like this:

      template <class A> class TJanitor {
      public:
          TJanitor( A* anA ) : p( anA ) {};
          TJanitor() : p( NIL ) {};
          ~TJanitor() { delete p; };
          TJanitor& operator =( A* anA ) { p = anA; };// Should throw exception if not NIL
          operator A*() { return p; };                // convenience
          A* operator ->() { return p; };            // convenience
      private:
          A* p;
          TJanitor( const TJanitor& );                // no copies
          TJanitor& operator=( const TJanitor& );     // no assignments
      };
Then you can use it like this:

      void Foo() {
          TJanitor<TBar> p1, p2, p3;
      
          p1 = new TBar;
          p2 = new TBar;
          p3 = new TBar;
          ...
      }
As it stands, this class does not implement smart pointers, and so doesn't deal with more than one TJanitor pointing at the same object. Smart pointers require considerably more work. The Taligent Application Environment has two classes like TJanitor that you can use:
TDeleterFor<> and TDeleterForArrayOf<>.

NOTE Although the C++ books state that operator new returns 0 when allocation fails, the ISO/ANSI committee has decided to change this to throw an exception, and that is what Taligent implements. This is somewhat moot as it is difficult with a heavily heap-oriented architecture to recover from out-of-memory conditions (at least in the default heap; other heaps are not a problem). Taligent is considering a MacApp-like scheme (prohibit all but critical allocations when available space gets low), but you should still expect operator new to throw
an exception.


[Contents] [Previous] [Next]
Click the icon to mail questions or corrections about this material to Taligent personnel.
Copyright©1995 Taligent,Inc. All rights reserved.

Generated with WebMaker