Exception Safety of C++ Programs

2006年6月26日

Exception Safety of C++ Programs

Robbie Mosaic, 2006-06-26

Today, let me talk about exception safety of C++ programs. As a program respond to exceptions, as said in The Art Of Unix Programming, the program should either repair the error automatically, or report the error noisily and quit when it cannot repair.

Reparing must make the environment consistent. One easy way to do repairing is to make a duplication of the original data, and use this duplication to restore the state of the program when an error occurs. This can be done in many programs that does computation, but it cannot guarantee programs that use external devices (files or databases) recover completely. Another way is to store and use roll-back information. This way is better when performance would be greatly affected by making duplication, but storing roll-back information is also time-consuming. This can work with external devices, or you can combine duplication of internal data and roll-back of external devices together.

Besides things talked above, the rolling back also includes releasing unused resources. In this sense, the C++ destructor does this (when in C programming, you have to write a destroy_xxx function). However, what is worth noticing is that C++ doesn’t support the finally keyword. The finally keyword is supported in Java and C#, and can be simulated using the goto statement in C (so we shouldn’t barely neglect goto). In C++, in order to make resource releasing automatic even when an error occurs, you should use destructors.

Classes in the STL are already exception-safe, so you don’t have to worry about them. What you need to protect with destructors, are traditional style resource allocations, such as fopen, new and so on. Note that new is also of traditional style—it requires explicit use of delete. To do the protection, we build guard classes and rely on the stack unwinding mechanism of C++. See the code below:

void test()
{
    FILE *fp = NULL;
    int ch;

    fp = fopen("xxx.txt", "r");
    if (fp == NULL) {
        throw string("Error opening the file xxx.txt in test.n");
    }
    FileHandleGuard fpgd(&fp); /* guard for fp */
    while ((ch = getc(fp)) != NULL) {
        putchar(ch);
    }
    fclose(fp); /* these two statements can be ignored */
    fp = NULL;
}

The FileHandleGuard class is as follows:

/* FileHandleGuard.h */
class FileHandleGuard
{
private:
    FILE **m_fp;

public:
    FileHandleGuard(FILE **fp);
    virtual ~FileHandleGuard();
};

/* FileHandleGuard.c */
FileHandleGuard::FileHandleGuard(FILE **fp)
{
    m_fp = fp;
}

FileHandleGuard::~FileHandleGuard()
{
    if (*m_fp != NULL) {
        fclose(*m_fp);
    }
}

That’s it. The FileHandleGuard object fpgd is constructed after the opening of the file, so it will be destructed before the function exits, and also before the destruction of fp. Note that if you close the file using fclose in the function, you must also set fp to NULL.

In some situations, such as a guarded object is of no common type, but of a common interface, you can make the guard class a template class. For example, when you import classes from the MSWord.olb object library using Visual C++, you will find that each of these classes has two methods: AttachDispatch and ReleaseDispatch. There are classes such as _Application, _Document and Documents. You can use a template guard class to guard against objects of these classes.

In C you can also apply the stack unwinding semantic instead of the finally semantic. However, it is error-prone to do stack unwinding by hand.

Above I demonstrated the use of guard classes. Here I will mention something about error messages. In the code:

if (fp == NULL) {
    throw string("Error opening the file xxx.txt in test.n");
}

A string object is thrown out there. However, this information is sometimes not enough when it is ultimately reported to the user. In this case, you can include more information along the stack in outer functions.

void test2(int n)
{
    int i;

    try {
        for (i = 0; i < n; i++) {
            test();
        }
    } catch (string s) {
        ostringstream ss;
        ss << s << "Time: doing loop number " << i << " in test2.n";
        throw ss.str();
    }
}

Thus, the loop number is also stored, and this may be helpful sometimes.

留下您的评论