Error Handling with C++ Exceptions, Part 2



December 01, 1997
URL:http://www.drdobbs.com/error-handling-with-c-exceptions-part-2/184403429

December 1997/Error Handling with C++ Exceptions, Part 2

Exceptions help you unwind from errors without losing track of resources, provided you use them with a proper discipline.


Last month I introduced C++ exception handling in the larger context of error handling in general. I conclude this month by investigating the problem of resource failure in the presence of exceptions, and share a useful strategy for handling all errors in C++.

While stack unwinding takes care of destroying automatic variables, there may still be some cleanup left to do. For example, the file opened in Listing 1 will not get closed if you throw an exception in the middle of f. One way to guarantee that a resource will be deallocated is to catch all exceptions in the function where the deallocation takes place. The handler in f in Listing 2 closes the file and then passes whatever exception occurred on up the line by re-throwing it. (That's what throw without an argument does.) This technique could be tedious, however, if you use the same resource in many places.

A better method is to arrange things so that stack unwinding will deallocate the resource automatically. In Listing 3, I create a class File whose constructor opens the file and whose destructor closes it. Since the File object x is automatic, its destructor is guaranteed to execute as the stack unwinds. You can use this technique, which Bjarne Stroustrup has named "Resource Allocation is Initialization," to safely handle any number of resources.

One of the motivations for adding exceptions to the language was to compensate for the fact that constructors have no return value. Before exceptions, it was very awkward to handle a resource error in a constructor. For example, what happens if the call to fopen fails in Listing 3? One solution uses an internal state variable which you test before using the resource. The program in Listing 4 uses the file pointer returned from fopen to determine if the resource is available or not. The void* conversion operator returns a nonzero pointer value if all is well, so you can test the state like this:

if (x)
    // All is well
else
    // Resource unstable

Having to test an object before each use can be quite tedious. As Listing 5 illustrates, it may be more convenient to throw an exception directly from the constructor.

Everything is fine as long as all your File objects are automatic, but what if you allocate one from the free store? As the program in Listing 6 illustrates, since a File* is just a pointer, no destructor is called. Since allocating dynamic objects is so common in C++, the standard library supplies a smart pointer that solves this problem. auto_ptr is a template class, declared in <memory>, with the following interface:

template<class T> class auto_ptr
{
  public:
    explicit auto_ptr(T* = 0);
    auto_ptr(auto_ptr<T>&);
    void operator=(auto_ptr<T>& r);
    // calls delete on underlying pointer
    ~auto_ptr();
    // calls T::operator*
    T& operator*();
    // calls T::operator->
    T* operator->();
    // returns the underlying pointer
    T* get() const;
    // loses pointer
    T* release();
    // releases old owner first
    T* reset(T* = 0);
};

As you can see in Listing 7 you can "wrap" the result of a new expression in an auto_ptr object. auto_ptr's operator* and operator-> members forward the respective operations on to the underlying pointer, so you can use it normally. Since the auto_ptr object itself resides on the stack, its destructor is called, which in turn deletes the T* object it owns. There is a one-to-one relationship between each auto_ptr and each pointer owned by any auto_ptr.

Recall that when you create an object on the free store, as in

T* p = new T;

the system calls operator new to allocate the memory for the new object before initializing it. If the constructor T() throws an exception, you don't need to be concerned about a memory leak — the runtime system calls operator delete to free the memory that the call to operator new allocated. C++ always cleans up partially created objects when an exception occurs. (Note: Not all compilers support this feature yet.)

Memory Management

Before exceptions, if a new operation failed to allocate an object, it returned a null pointer, just like malloc does in C:

T *tp = new T;
if (tp)
    // Use new object

C++ now stipulates that a memory allocation failure throws a bad_alloc exception. The draft Standard C++ library as well as third-party libraries may make generous use of the free store. Since a memory allocation request can occur when you least expect it, most any program you write should be prepared to handle a bad_alloc exception. The obvious way is to supply a handler:

#include <new>
catch(const bad_alloc& x)
{
    cerr << "Out of memory: " << x.what() << endl;
    abort();
}

Depending on your application, you may be able to do something more interesting than this handler does, such as recover some memory and return to some stable state to try again.

If you prefer the classic behavior of returning a null pointer, you can use placement new with the predefined object nothrow, as follows:

#include <new>
// ...
    T* tp = new (nothrow) T;
    if (tp)
        // use tp...

Yet another way of handling out-of-memory conditions is to replace parts of the memory allocation machinery itself. When a memory allocation fails, C++ calls the default new handler, which in turn throws a bad_alloc exception. You can provide your own handler by passing its address to set_new_handler, much as I did with set_terminate in last month's article (see Listing 3 of that article).

Exception Specifications

You can enumerate the exceptions that a function will throw with an exception specification:

class A;
class B;
     
void f() throw(A,B)
{
    // Whatever
}

This definition states that while f is executing, only exceptions of type A or B will be thrown (and not caught inside f). Besides being good documentation, exception specifications ensure that only the allowable types of exceptions propagate out of f, whether they occur directly in f or indirectly from deeper down the call chain. In the presence of any other exception, control passes to the standard library function unexpected, which by default terminates the program. The definition of f above, therefore, is equivalent to:

void f()
{
    try
    {
        // Whatever
    }
    catch(const A&)
    {
        throw;       // rethrow
    }
    catch(const B&)
    {
        throw;       // rethrow
    }
    catch(...)
    {
        unexpected();
    }
}

You can provide your own unexpected handler by passing a pointer to it to the standard library function set_unexpected (see Listing 8) . The definition:

void f() throw()
{
    // Whatever
}

disallows any exceptions while f is executing; that is, it is equivalent to:

void f()
{
    try
    {
        // Whatever
    }
    catch(...)
    {
        unexpected();
    }
}

A function without an exception specification can throw any exception.

The challenge with exception specifications is that f may call other functions that throw other exceptions. You must know what exceptions are possible and either include them in the specification for f or handle them explicitly within f itself. This can be a problem if a future version of the services f uses adds new exceptions. If those new exceptions are derived from A or B, you don't have a problem, but that is not likely to happen with commercial libraries.

A good rule of thumb: if a function f calls another function which does not have an associated exception specification, then don't declare one for f. Also, since template parameters can usually be of almost any type, you may not be able to predict which exceptions might occur from that type's member functions. Bottom line: templates and exception specifications don't mix.

An Error-Handling Strategy

There is one category of errors I haven't yet discussed, which I like to refer to as My Stupid Mistakes. The first person possessive adjective is significant here. Whether you know it or not (but I hope you do), all developers make logical assumptions as they construct software. For example, there are many times when I can say, "this pointer can't be null here," and I know it will always be true, if I have crafted things the way I intended. To make the assertion explicit and enforceable, I insert the following statement to say so:

assert(p);

The assert macro, defined in <assert.h>, will abort the program with an error message indicating the offending line number and file name if the expression in parentheses evaluates to zero. When I use the assert macro, it means that I have control over the conditions that govern the assert expression. Other developers like to use assertions to check for user errors, or even for pre-conditions on a parameter, but I think this is a mistake. An assertion is just that, an assertion, and if I have no control over the assertion, I shouldn't make it. A more involved example might help to illustrate.

Consider the following function from an object-oriented persistence framework that writes an object's data record to a database:

bool Update::Write()
{
    CRecordset* pRS = m_pTableX->GetRecordset();
    assert(pRS);
    if (!pRS->CanUpdate())
        Throw2(PFX,UPDATE_ERROR,
               "Database not updatable");
     
    bool status;
    try
    {
        status = pRS->Update();
    }
    catch (CDBException *ep)
    {
        string msg =
            ep->m_strError + ep->m_strStateNativeOrigin;
        ep->Delete();
        Throw2(PFX,UPDATE_ERROR,msg);
    }
     
    return status;
}

The function GetRecordset returns a pointer to a recordset, a Microsoft Foundation Class library abstraction for doing relational database I/O. I have designed this system so that pRS can't be null. If it is, then my software is broken. On the other hand, I have no control over whether the connection to the database that you have provided is updatable. So I throw an exception instead (I'll explain the Throw2 macro shortly). This gives you, my client, an opportunity to fix things at run time, if you plan ahead, instead of having to quit your program and have your user start over. I look upon input arguments to a function the same way. So, my first suggestions for error handling are:

1. Make assertions liberally, but only for things you have first-hand control over.

2. Throw exceptions for all other errors, including invalid arguments.

Notice that in the example above I handle database errors by throwing my own brand of exception. That's because my clients don't need to know about the underlying mechanism I use to access the database. All they know is that they are using my component, and my component can throw exceptions. All they have to do, then, is to catch the exceptions I throw, or that the Standard C++ library might (like memory failure, which I don't catch), like this:

try
{
    // call one of my functions here:
    p->Write();
}
catch (PFX_Exception& x)
{
    // Do whatever you have to; here I just print a message:
    cout << "PFX exception: " << x.what() << endl;
}
catch (exception& x)
{
    cout << "C++ exception: " << x.what() << endl;
}
catch (...)
{
    cout << "Unknown exception" << endl;
}

My persistence component has a number of classes, but it has only one exception class, which derives from exception (see Listings 9 and 10) . Having more than one exception class per component just makes things complicated for clients. In addition, I have all my exceptions include in their what message the file name and line number where the exception was thrown from, since that important information is not otherwise available. To do this, I borrow a preprocessor trick that assert uses. The Throw macro in Listing 11 transforms a call such as:

Thow(PFX,LOCK_ERROR);

into the statement:

throw PFX_Exception(???);

where ??? represents a string argument that includes the current file name and line number, using the predefined macros __FILE__ and __LINE__, respectively. PFX_Exception derives from exception and supplies predefined strings corresponding to integer error codes (like LOCK_ERROR) to the exception constructor. The stringizing preprocessor operator # in Listing 11 effectively puts quotes around its argument, and the token-pasting operator ## combines its arguments into a single preprocessor token. Since this isn't an article on the preprocessor, I'll let you stare at the definition of Throw and hope it makes sense. Throw2 allows you to add extra text at the throw point. For example, if you caught an exception thrown by the expression:

Throw2(PFX,RECORDSET_ERROR,"Extra Text");

then the what string for the exception might be:

Recordset Open Error:Extra Text:txcept.cpp:Line 18

Summary

Perhaps the most important thing to say about exceptions is that you should use them only in truly exceptional circumstances. Like setjmp and longjmp, they interrupt the normal control flow of a program. There should be relatively few exception handlers compared to the number of functions in a typical program. In addition, exceptions have been designed for synchronous events only, so beware; exceptions and signals don't mix. As far as efficiency is concerned, experience so far suggests that exceptions bloat your code size by 5 - 10% for each try block and its associated nested functions. You typically pay in speed, though, only if exceptions get thrown.

I end with a few guidelines:

This article is based on material from the author's forthcoming book, C and C++ Code Capsules: A Guide for Practitioners, Prentice-Hall, 1998. o

Chuck Allison is Consulting Editor and a former columnist with CUJ. He is the owner of Fresh Sources, a company specializing in object-oriented software development, training, and mentoring. He has been a contributing member of J16, the C++ Standards Committee, since 1991, and is the author of C and C++ Code Capsules: A Guide for Practitioners, Prentice-Hall, 1998. You can email Chuck at [email protected].

December 1997/Error Handling with C++ Exceptions, Part 2/Listing 1

Listing 1: Illustrates a dangling resource

// destroy3.cpp
#include <stdio.h>

main()
{
    void f(const char*);
    try
    {
        f("destroy3.cpp");
    }
    catch(int)
    {
        puts("Caught exception");
    }
}

void f(const char* fname)
{
    FILE* fp = fopen(fname,"r");
    if (fp)
    {
        throw 1;
        fclose(fp);     // This won't happen
    }
}

// Output:
Caught exception
//End of File

December 1997/Error Handling with C++ Exceptions, Part 2/Listing 10

Listing 10: PFX_Exception Implementation

// pfxxcept.cpp

#include "pfxxcept.h"

string
PFX_Exception::s_ErrorStrings[NUM_ERRORS] =
{
    "Bad Objid",
    "Connection open failed",
    "Recordset open failed",
    "Recordset requery failed",
    "Recordset update failed",
    "Record lock failed",
    "Object out of date with database",
    "Transaction error",
    "Attempt to Write a ReadOnly object",
    "INI File name missing"
}; 
//End of File

December 1997/Error Handling with C++ Exceptions, Part 2/Listing 11

Listing 11: Handy exception macros

// xcept.h:    Useful throw macros

#include <exception>
#include <string>

// Macro trick to quote a number:
#define str_(x) #x
#define xstr_(x) str_(x)

// Macro for convenient exception throwing
// (includes standard message, file, line #)
#define Throw(Type, cod) \
throw Type ## _Exception(Type ## _Exception:: ## cod, \
                         std::string(__FILE__ ## ":Line "
                         ## xstr_(__LINE__)))

#define Throw2(Type, cod, txt) \
throw Type ## _Exception(Type ## _Exception:: ## cod, \
                         txt + std::string(":" ## \
                         __FILE__ ## ":Line " ## \
                         xstr_(__LINE__)))
//End of File

December 1997/Error Handling with C++ Exceptions, Part 2/Listing 2

Listing 2: Deallocates a resource in the midst of handling an exception

// destroy4.cpp
#include <stdio.h>

main()
{
    void f(const char*);
    try
    {
        f("file1.dat");
    }
    catch(int)
    {
        puts("Caught exception");
    }
}

void f(const char* fname)
{
    FILE* fp = fopen(fname,"r");
    if (fp)
    {
        try
        {
            throw 1;
        }
        catch(int)
        {
            fclose(fp);
            puts("File closed");
            throw;  // Re-throw
        }

        fclose(fp); // The normal close
    }
}

// Output:
File closed
Caught exception
//End of File

December 1997/Error Handling with C++ Exceptions, Part 2/Listing 3

Listing 3: Illustrates the principle of "Resource Allocation is Initialization"

// destroy5.cpp
#include <stdio.h>

main()
{
    void f(const char*);
    try
    {
        f("file1.dat");
    }
    catch(int)
    {
        puts("Caught exception");
    }
}

void f(const char* fname)
{
    class File
    {
        FILE* f;
    public:
        File(const char* fname,
             const char* mode)
        {
            f = fopen(fname, mode);
        }
        ~File()
        {
            fclose(f);
            puts("File closed");
        }
    };

    File x(fname,"r");
    throw 1;
}

// Output:
File closed
Caught exception
//End of File

December 1997/Error Handling with C++ Exceptions, Part 2/Listing 4

Listing 4: Uses internal state to track a resource

// destroy6.cpp
#include <stdio.h>

main()
{
    void f(const char*);
    try
    {
        f("file1.dat");
    }
    catch(int)
    {
        puts("Caught exception");
    }
}

void f(const char *fname)
{
    class File
    {
        FILE* f;
    public:
        File(const char* fname,
             const char* mode)
        {
            f = fopen(fname, mode);
        }
        ~File()
        {
            if (f)
            {
                fclose(f);
                puts("File closed");
            }
        }
        operator void*() const
        {
            return f ? (void *) this : 0;
        }
    };
    File x(fname,"r");
    if (x)
    {
        // Use file here
        puts("Processing file...");
    }
    throw 1;
}

// Output:
Processing file...
File closed
Caught exception
//End of File

December 1997/Error Handling with C++ Exceptions, Part 2/Listing 5

Listing 5: Throws an exception in a constructor

// destroy7.cpp
#include <stdio.h>
    
class File
{
    FILE* f;

public:
    File(const char* fname, const char* mode)
    {
        f = fopen(fname, mode);
        if (!f)
            throw 1;
    }
    ~File()
    {
        if (f)
        {
            fclose(f);
            puts("File closed");
        }
    }
};

main()
{
    void f(const char*);
    try
    {
        f("file1.dat");
    }
    catch(int x)
    {
        printf("Caught exception: %d\n",x);
    }
}

void f(const char* fname)
{
    File x(fname,"r");
    puts("Processing file...");
    throw 2;
}

// Output:
Processing file...
File closed
Caught exception: 2
//End of File

December 1997/Error Handling with C++ Exceptions, Part 2/Listing 6

Listing 6: Reveals memory leak problems when using the new operator with exceptions

// destroy8.cpp
#include <stdio.h>
    
class File
{
    FILE* f;

public:
    File(const char* fname, const char* mode)
    {
        f = fopen(fname, mode);
        if (!f)
            throw 1;
    }
    ~File()
    {
        if (f)
        {
            fclose(f);
            puts("File closed");
        }
    }
};

main()
{
    void f(const char*);
    try
    {
        f("file1.dat");
    }
    catch(int x)
    {
        printf("Caught exception: %d\n",x);
    }
}

void f(const char* fname)
{
    File* xp = new File(fname,"r");
    puts("Processing file...");
    throw 2;
    delete xp;      // Won't happen
}

// Output:
Processing file...
Caught exception: 2
//End of File

December 1997/Error Handling with C++ Exceptions, Part 2/Listing 7

Listing 7: Fixes the memory leak with auto_ptr

// destroy9.cpp
#include <stdio.h>
#include <memory>       // For auto_ptr
    
using namespace std;

class File
{
    FILE* f;

public:
    File(const char* fname, const char* mode)
    {
        f = fopen(fname, mode);
        if (!f)
            throw 1;
    }
    ~File()
    {
        if (f)
        {
            fclose(f);
            puts("File closed");
        }
    }
};

main()
{
    void f(const char*);
    try
    {
        f("file1.dat");
    }
    catch(int x)
    {
        printf("Caught exception: %d\n",x);
    }
}

void f(const char* fname)
{
    auto_ptr<File> xp = new File(fname,"r");
    puts("Processing file...");
    throw 2;
}

// Output:
Processing file...
File closed
Caught exception: 2
//End of File

Terms of Service | Privacy Statement | Copyright © 2024 UBM Tech, All rights reserved.