16. Exception Handling

Don’t Panic!

The Hitchhiker’s Guide to the Galaxy

Aims for exception handling — assumptions about exception handling — syntax — grouping of exceptions — resource management — errors in constructors — resumption vs. termination semantics — asynchronous events — multi-level exception propagation — static checking — implementation issues — invariants.

16.1 Introduction

In the original design of C++, exceptions were considered, but postponed because there wasn’t time to do a thorough job of exploring the design and implementation issues and because of fear of the complexity they might add to an implementation (§3.15). In particular, it was understood that a poor design could cause run-time overhead and a significant increase in porting times. Exceptions were considered important for error handling in programs composed out of separately designed libraries.

The actual design of the C++ exception mechanism stretched over years (1984 to 1989) and was the first part of C++ to be designed in the full glare of public interest. In addition to the innumerable blackboard iterations that every C++ feature went through, several designs were worked out on paper and widely discussed. Andrew Koenig was closely involved in the later iterations and is the coauthor (with me) on the published papers [Koenig, 1989a] [Koenig, 1990]. Andy and I worked out significant parts of the final scheme en route to the Santa Fe USENIX C++ conference in November 1987. I also had meetings at Apple, DEC (Spring Brook), Microsoft, IBM (Almaden), Sun, and other places where I presented draft versions of the design and received valuable input. In particular, I searched out people with real experience with systems that provide exception handling to compensate for my personal inexperience in that area. The first serious discussion of exception handling for C++ that I recall was in Oxford in the summer of 1983. The focus of that discussion with Tony Williams from the Rutherford Lab was the design of fault-tolerant systems and the value of static checking in exception-handling mechanisms.

At the time when the debate about exception handling started in the ANSI C++ committee, experience with exception handling in C++ was limited to library-based implementations by Apple, Mike Miller [Miller, 1988], and others, and to a single compiler-based implementation by Mike Tiemann [Tiemann,1990]. This was worrying, though there was fairly wide agreement that exception handling in some suitable form was a good idea for C++. In particular, Dmitry Lenkov expressed a strong wish for exception handling based on experiences at Hewlett-Packard. A notable exception to this agreement was Doug McIlroy, who stated that the availability of exception handling would make systems less reliable because library writers and other programmers will throw exceptions rather than try to understand and handle problems. Only time will tell to what extent Doug’s prediction will be true. Naturally, no language feature can prevent programmers from writing bad code.

The first implementations of exception handling as defined in the ARM started appearing in the spring of 1992.

16.2 Aims and Assumptions

The following assumptions were made for the design:

– Exceptions are used primarily for error handling.

– Exception handlers are rare compared to function definitions.

– Exceptions occur infrequently compared to function calls.

– Exceptions are a language-level concept – not just implementation, and not an error-handling policy.

This formulation, like the list of ideals below, is taken from slides I used for presentations of the evolving design from about 1988.

What is meant is that exception handling

– Isn’t intended as simply an alternative return mechanism (as was suggested by some, notably David Cheriton), but specifically as a mechanism for supporting the construction of fault-tolerant systems.

– Isn’t intended to turn every function into a fault-tolerant entity, but rather as a mechanism by which a subsystem can be given a large measure of fault tolerance even if its individual functions are written without regard for overall error-handling strategies.

– Isn’t meant to constrain designers to a single “correct” notion of error handling, but to make the language more expressive.

Throughout the design effort, there was an increasing influence of systems designers of all sorts and a decrease of input from the language design community. In retrospect, the greatest influence on the C++ exception handling design was the work on fault-tolerant systems started at the University of Newcastle in England by Brian Ran-dell and his colleagues in the seventies and continued in many places since.

The following ideals evolved for C++ exception handling:

[1] Type-safe transmission of arbitrary amounts of information from a throw-point to a handler.

[2] No added cost (in time or space) to code that does not throw an exception.

[3] A guarantee that every exception raised is caught by an appropriate handler.

[4] A way of grouping exceptions so that handlers can be written to catch groups of exceptions as well as individual ones.

[5] A mechanism that by default will work correctly in a multi-threaded program.

[6] A mechanism that allows cooperation with other languages, especially with C.

[7] Easy to use.

[8] Easy to implement.

Most of these ideals were achieved, others ([3], [8]) were considered too expensive or too constraining and were only approximated. I consider it given that error handling is a difficult task for which the programmer needs all the help that can be provided. An over-zealous language designer might provide features and/or constraints that would actually complicate the task of designing and implementing a fault-tolerant system.

My view that fault-tolerant systems must be multi-level helped me resist the clamor for “advanced” features. No single unit of a system can recover from every error that might happen in it, and every bit of violence that might be done to it from “the outside.” In extreme cases, power will fail or a memory location will change its value for no apparent reason.

At some point, the unit must give up and leave further cleanup to a “higher” unit. For example, a function may report a catastrophic failure to a caller, a process may have to terminate abnormally and leave recovery to some other process, a processor may ask for help from another, and a complete computer may have to request help from a human operator. Given this view, it makes sense to emphasize that the error handling at each level should be designed so that relatively simple code using relatively simple exception handling features will have a chance of actually working.

Trying to provide facilities that allow a single program to recover from all errors is misguided and leads to error-handling strategies so complex that they themselves become a source of errors.

16.3 Syntax

As ever, syntax attracted more attention than its importance warranted. In the end, I settled on a rather verbose syntax using three keywords and lots of brackets:

int f()
{
    try {           // start of try block
        return g();
    }
    catch (xxii) {  // start of exception handler

            // we get here only if 'xxii' occurs
        error("g() goofed: xxii");
        return 22;
    }
}

int g()
{
    // ...
    if (something_wrong) throw xxii();  // throw exception
    // ...
}

The try keyword is completely redundant and so are the { } brackets except where multiple statements are actually used in a try-block or a handler. For example, it would have been trivial to allow:

int f()
{
    return g() catch (xxii) {  // not C++
        error (*'g() goofed: xxii");
        return 22;
    };
}

However, I found this so difficult to explain that the redundancy was introduced to save support staff from confused users. Because of the C community’s traditional aversion to keywords, I tried hard to avoid having three new keywords for exception handling, but every scheme I cooked up with fewer keywords seemed overly clever and/or confusing. For example, I tried to use catch for both throwing an exception and for catching it. This can be made logical and consistent, but I despaired over explaining that scheme.

The word throw was chosen partly because the more obvious words raise and signal had already been taken by standard C library functions.

16.4 Grouping

Having talked to dozens of users of more than a dozen different systems supporting some form of exception handling, I concluded that the ability to define groups of exceptions is essential. For example, a user must be able to catch “any I/O library exception” without knowing exactly which exceptions that includes. There are workarounds when a grouping mechanism isn’t available. For example, one might encode what would otherwise have been different exceptions as data carried by a single exception, or simply list all exceptions of what we consider a group everywhere a catch of the group is intended. However, every such workaround was experienced – by most if not everybody – to be a maintenance problem.

Andrew Koenig and I first tried a grouping scheme based on groups dynamically constructed by constructors for exception objects. However, this seemed somewhat out of style with the rest of C++ and many people, including Ted Goldstein and Peter Deutsch, noted that most such groups were equivalent to class hierarchies. We therefore adopted a scheme inspired by ML where you throw an object and catch it by a handler declared to accept objects of that type. The usual C++ initialization rules then allow a handler for a type B to catch objects of any class D derived from B. For example:

class Matherr { };
class Overflow: public Matherr { };
class Underflow: public Matherr { };
class Zerodivide: public Matherr { };
// ...


void g()
{
    try {
        f ();
    }
    catch (Overflow) {
        // handle Overflow or anything derived from Overflow
    }
    catch (Matherr) {
        // handle any Matherr that is not Overflow
    }
}

It was later discovered that multiple inheritance (§12) provided an elegant solution to otherwise difficult classification problems. For example, one can declare a network file error like this:

class network_file_err
    : public network_err , public file_system_err { };

An exception of type network_file_err can be handled both by software expecting network errors and software expecting file system errors. I believe that Daniel Weinreb was the first one to spot this usage.

16.5 Resource Management

The central point in the exception handling design was the management of resources. In particular, if a function grabs a resource, how can the language help the user to ensure that the resource is correctly released upon exit even if an exception occurs? Consider this simple example borrowed from [2nd]:

void use_file(const char* fn)
{
    FILE* f = fopen(fn,"w");  // open file fn
    
    // use f

    fclose(f);  // close file fn
}

This looks plausible. However, if something goes wrong after the call of fopen() and before the call of fclose(), an exception may cause use_file() to be exited without calling fclose(). Please note that exactly the same problem can occur in languages that do not support exception handling. For example, a call of the standard C library function longjmp() would have the same bad effects. If we want to write a fault-tolerant system, we must solve this problem. A primitive solution looks like this:

void use_file(const char* fn)
{
    FILE* f = fopen(fn,"r");  // open file fn
    try {
        // use f
    }
    catch (...) {   // catch all
        fclose(f);  // close file fn
        throw;      // re-throw
    }
    fclose(f);   // close file fn
}

All the code using the file is enclosed in a try block that catches every exception, closes the file, and re-throws the exception.

The problem with this solution is that it is verbose, tedious, and potentially expensive. Furthermore, any verbose and tedious solution is error-prone because programmers get bored. We can make this solution ever so slightly less tedious by providing a specific finalization mechanism to avoid the duplication of the code releasing the resource (in this case fclose(f)), but that does nothing to address the fundamental problem: writing resilient code requires special and more complicated code than traditional code.

Fortunately, there is a more elegant solution. The general form of the problem looks like this:

void use()
{
    // acquire resource 1
    // ...
    // acquire resource n
    
    // use resources

    // release resource n
    // ...
    // release resource 1
}

It is typically important that resources are released in the reverse order of their acquisition. This strongly resembles the behavior of local objects created by constructors and destroyed by destructors. Thus we can handle such resource acquisition and release problems by a suitable use of objects of classes with constructors and destructors. For example, we can define a class FilePtr that acts like a FILE*:

class FilePtr {
    FILE* p;
public:
    FilePtr(const char* n, const char* a) { p = fopen(n,a); }
    FilePtr(FILE* pp) { p = pp; }
    ~FilePtr() { fclose(p); }

    operator FILE*() { return p; }
};

We can construct a FilePtr given either a FILE* or the arguments required for fopen(). In either case, a FilePtr will be destroyed at the end of its scope and its destructor closes the file. Our program now shrinks to this minimum

void use_file(const char* fn)
{
    FilePtr f(fn,"r");  // open file fn
    // use f
} // file fn implicitly closed

and the destructor will be called independently of whether the function is exited normally or because an exception is thrown.

I called this technique “resource acquisition is initialization.” It extends to partially constructed objects and thus addresses the otherwise difficult issue of what to do when an error is encountered in a constructor; see [Koenig, 1990] or [2nd].

16.5.1 Errors in Constructors

To some, the most important aspect of exceptions is that they provide a general mechanism for reporting errors detected in a constructor. Consider the constructor for FilePtr; it didn’t test whether the file was opened correctly. A more careful coding would be:

FilePtr::FilePtr(const char* n, const char* a)
{
    if ((p = fopen(n,a)) == 0) {
        // oops! open failed – what now?
    }
}

Without exceptions, there is no direct way of reporting the failure because a constructor doesn’t have a return value. This has led people to use workarounds such as putting the constructed objects into an error state, leaving return value indicators in agreed upon variables, etc. Surprisingly enough, this was rarely a significant practical problem. However, exceptions provide a general solution:

FilePtr::FilePtr(const char* n, const char* a)
{
    if ((p = fopen(n,a)) == 0) {
        // oops! open failed
        throw Open_failed(n,a);
    }
}

Importantly, the C++ exception handling mechanism guarantees that partly constructed objects are correctly destroyed, that is, completely constructed sub-objects are destroyed and yet-to-be-constructed sub-objects are not. This allows the writer of a constructor to concentrate on the error handling for the object in which the failure is detected. For details see [2nd,§9.4.1].

16.6 Resumption vs. Termination

During the design of the exception handling mechanism, the most contentious issue turned out to be whether it should support termination semantics or resumption semantics; that is, whether it should be possible for an exception handler to require execution to resume from the point where the exception was thrown. For example, wouldn’t it be a good idea to have the routine invoked because of memory exhaustion, find some extra memory, and then return? To have the routine invoked because of a divide-by-zero return with a user-defined value? To have the routine invoked because a read routine found the floppy drive empty, request the user to insert a disk, and then return?

My personal starting point was: “Why not? That seems a useful feature. I can see quite a few situations where I could use resumption.” Over the next four years, I learned otherwise, and thus the C++ exception handling mechanism embodies the opposite view, often called the termination model.

The main resumption vs. termination debate took place in the ANSI C++ committee where the issue was discussed in the committee as a whole, in the extensions working group, at evening technical sessions, and on the committee’s electronic mailing lists. That debate lasted from December 1989 when the ANSI committee was formed to November 1990. Naturally, the issues were also the topic of much interest in the C++ community at large. In the committee, the resumption point of view was ably presented and defended primarily by Martin O’Riordan and Mike Miller. Andrew Koenig, Mike Vilot, Ted Goldstein, Dag Brück, Dmitry Lenkov, and I were usually the most vocal proponents of termination semantics. I conducted most of the discussions in my role as chairman of the extensions working group. The outcome was a 22 to 1 vote for termination semantics in the extensions working group after a long meeting where experience data was presented by representatives of DEC, Sun, TI, and IBM. This was followed by the acceptance of the exception handling proposal as presented in the ARM (that is, with termination semantics) by a 30 to 4 vote by the full committee.

After a long debate at the Seattle meeting in July 1990, I summarized the arguments for resumption like this:

– More general (powerful, includes termination).

– Unifies similar concepts/implementations.

– Essential for very complex, very dynamic systems (that is, OS/2).

– Not significantly more complex/expensive to implement.

– If you don’t have it, you must fake it.

– Provides simple solutions for resource exhaustion problems.

The arguments for termination were similarly summarized:

– Simpler, cleaner, cheaper.

– Leads to more manageable systems.

– Powerful enough for everything.

– Avoids horrendous coding tricks.

– Significant negative experience with resumption.

These lists trivialize the debate, which was very technical and thorough. It also got quite heated at times with less restrained proponents expressing the view that termination proponents were somehow trying to impose an arbitrary and restrictive view of programming on them. Clearly, the termination/resumption issue touches deep issues of how software ought to be designed. The debate was never between two equal-sized groups. The proponents of termination semantics always seemed to be in a 4-to-l or larger majority in every forum.

The most repeated and most persuasive arguments for resumption were that

[1] because resumption is a more general mechanism than termination, it should be accepted even if there was doubt about the usefulness;

[2] there are important cases where a routine finds itself blocked because of the lack of a resource (for example, memory exhaustion or an empty floppy disk drive). In that case, resumption will allow the routine to throw an exception, have the exception handler provide the missing resource, and then resume the execution as if the resource had never been missing.

The most repeated and convincing arguments (to me) for termination were that

[1] Termination is significantly simpler than resumption. In fact, resumption requires the key mechanisms for continuations and nested functions without providing the benefits of those mechanisms.

[2] The method of dealing with resource exhaustion proposed in argument [2] for resumption is fundamentally bad. It leads to error-prone and hard-to-comprehend tight bindings between library code and users.

[3] Really major systems in many application areas have been written using termination semantics so resumption cannot be necessary.

The last point is also backed up by a theoretical argument by Flaviu Cristian that given termination, resumption isn’t needed [Cristian, 1989].

After a couple of years of discussion, I was left with the impression that one could concoct a convincing logical argument for either position. Even the original paper on exception handling [Goodenough,1975] had done so. We were in the position of the ancient Greek philosophers debating the nature of the universe with such intensity and subtlety that they forgot to study it. Consequently, I kept asking anyone with genuine experience with large systems to come forward with data. On the side of resumption, Martin O’Riordan reported that “Microsoft had several years of positive experience with resumable exception handling,” but the absence of specific examples and doubts about the value of OS/2 Release 1 as a proof of technical soundness weakened his case. Experiences with PL/I’s ON-conditions were mentioned as arguments both for and against resumption.

Then, at the Palo Alto meeting in November 1991, we heard a brilliant summary of the arguments for termination semantics backed with both personal experience and data from Jim Mitchell (from Sun, formerly from Xerox PARC). Jim had used exception handling in half a dozen languages over a period of 20 years and was an early proponent of resumption semantics as one of the main designers and implementers of Xerox’s Cedar/Mesa system. His message was

“termination is preferred over resumption; this is not a matter of opinion but a matter of years of experience. Resumption is seductive, but not valid.”

He backed this statement with experience from several operating systems. The key example was Cedar/Mesa: It was written by people who liked and used resumption, but after ten years of use, there was only one use of resumption left in the half million line system – and that was a context inquiry. Because resumption wasn’t actually necessary for such a context inquiry, they removed it and found a significant speed increase in that part of the system. In each and every case where resumption had been used it had – over the ten years – become a problem and a more appropriate design had replaced it. Basically, every use of resumption had represented a failure to keep separate levels of abstraction disjoint.

Mary Fontana presented similar data from the TI Explorer system where resumption was found to be used for debugging only, Aron Insinga presented evidence of the very limited and nonessential use of resumption in DEC’s VMS, and Kim Knuttilla related exactly the same story as Jim Mitchell for two large and long-lived projects inside IBM. To this we added a strong opinion in favor of termination based on experience at L.M.Ericsson relayed to us by Dag Brück.

Thus, the C++ committee endorsed termination semantics.

16.6.1 Workarounds for Resumption

It appears that most of the benefits of resumption can be obtained by combining a function call and a (terminating) exception. Consider a function that a user calls to acquire some resource X:

X* grab_X() // acquire resource X
{
    for (;;) {
        if (can_acquire_an_X) {
            // ...
            return some_X;
        }

        // oops! can't acquire an X, try to recover:

        grab_X_failed();
    }
}

It is the job of grab_X_failed() to make it possible to make an X available for acquisition. If it can’t, it can throw an exception:

void grab_X_failed()
{
    if (can_make_X_available) { // recovery
        // make X available
        return;
    }

    throw Cannot_get_X; // give up
}

This technique is a generalization of the new_handler approach to memory exhaustion (§10.6). There are, of course, many variants of this technique. My favorites use a pointer to function somewhere to allow a user to “tailor” the recovery action. This technique doesn’t burden the system with the complexity of a resumption implementation. Often, it doesn’t imply the negative impact on system organization that general resumption does.

16.7 Asynchronous Events

The C++ exception handling mechanism is explicitly not for handling asynchronous events directly:

“Can exceptions be used to handle things like signals? Almost certainly not in most C environments. The trouble is that C uses functions like malloc that are not re-entrant. If an interrupt occurs in the middle of malloc and causes an exception, there is no way to prevent the exception handler from executing malloe again.

A C++ implementation where calling sequences and the entire run-time library are designed around the requirement for re-entrancy would make it possible for signals to throw exceptions. Until such implementations are commonplace, if ever, we must recommend that exceptions and signals be kept strictly separate from a language point of view. In many cases, it will be reasonable to have signals and exceptions interact by having signals store away information that is regularly examined (polled) by some function that in turn may throw appropriate exceptions in response to the information stored by the signals [Koenig, 1990].”

My view, which appears to reflect a large majority view in the part of the C/C++ community concerned with exception handling, is that to produce reliable systems you need to map asynchronous events into some form of process model as quickly as possible. Having exceptions happen at random points in the execution and having to stop the processing of one exception to deal with an unrelated exception is a prescription for chaos. A low-level interrupt system should be separated from general programs as far as possible.

This view precludes the direct use of exceptions to represent something like hitting a DEL key and replacing UNIX signals with exceptions. In such cases, a low-level interrupt routine must somehow do its minimal job and possibly map into something that could trigger an exception at a well-defined point in a program’s execution. Note that signals, as defined in the C standard, are not allowed to call functions because during signal handling the machine state isn’t guaranteed to be consistent enough to handle a function call and return.

Similarly, low-level events, such as arithmetic overflows and divide by zero, are assumed to be handled by a dedicated lower-level mechanism rather than by exceptions. This enables C++ to match the behavior of other languages when it comes to arithmetic. It also avoids the problems that occur on heavily pipelined architectures where events such as divide by zero are asynchronous. Making divide by zero, etc., synchronous is not possible on all machines. Where it is possible, flushing the pipelines to ensure that such events are caught before unrelated computation has happened slows the machine down (often by an order of magnitude).

16.8 Multi-level Propagation

There are several good reasons to allow an exception to be implicitly propagated from a function to its immediate caller only. However, this was not an option for C++:

[1] There are millions of C++ functions that couldn’t reasonably be expected to be modified to propagate or handle exceptions.

[2] It is not a good idea to try to make every function a fire-wall. The best errorhandling strategies are those in which only designated major interfaces are concerned with non-local error handling issues.

[3] In a mixed-language environment, it is not possible to require a specific action of a function because that function may be written in another language. In particular, a C++ function throwing an exception may be called by a C function that was called by a C++ function willing to catch the exception.

The first reason is pragmatic, the other two are fundamental: [2] is a statement about systems design strategies, and [3] is a statement about what kind of environments C++ code is assumed to be able to work in.

16.9 Static Checking

By allowing multi-level propagation of exceptions, C++ loses one aspect of static checking. One cannot simply look at a function to determine which exceptions it may throw. In fact, it may in principle throw any exception even if there isn’t a single throw statement in the body of that function. Functions called by it may do the throwing.

Several people, notably Mike Powell, bemoaned this and tried to figure out how stronger guarantees could be provided for C++ exceptions. Ideally, we would like to guarantee that every exception thrown is caught by a suitable user-provided handler. Often, we would like to guarantee that only exceptions from an explicitly specified list can escape from a function. The C++ mechanism for specifying a list of exceptions that a function may throw was essentially designed by Mike Powell, Mike Tie-mann, and me on a blackboard at Sun sometime in 1989.

“In effect, writing this:

void f() throw (e1, e2)
{
    // stuff
}

is equivalent to writing this:

void f()
{
    try {
        // stuff
    }
    catch (el) {
        throw; // re-throw
    }
    catch (e2) {
        throw; // re-throw
    }
    catch (...) {
        unexpected();
    }
}

The advantage of the explicit declaration of exceptions that a function can throw over the equivalent checking in the code is not just that it saves typing. The most important advantage is that the function declaration belongs to an interface that is visible to its callers. Function definitions, on the other hand, are not universally available and even if we do have access to the source code of all our libraries we strongly prefer not to have to look at it very often.

“Another advantage is that it may still be practical to detect many uncaught exceptions during compilation [Koenig, 1990].”

Ideally, exception specifications would be checked at compile time, but that would require that every function cooperate in the scheme, and that isn’t feasible. Further, such static checking could easily become a source of much recompilation. Worse, such recompilation would only be feasible for users who had all the source code to recompile:

“For example, a function must potentially be changed and recompiled if a function it calls (directly or indirectly) changes the set of exceptions it catches or throws. This could lead to major delays in the production of software produced (partly) by composition of libraries from different sources. Such libraries would de facto have to agree on a set of exceptions to be used. For example, if subsystem X handles exceptions from subsystem Y and the supplier of Y introduces a new kind of exception, then X’s code will have to be modified to cope. A user of X and Y will not be able to upgrade to a new version of Y until X has been modified. Where many subsystems are used this can cause cascading delays. Even where the ‘multiple supplier problem’ does not exist this can lead to cascading modifications of code and to large amounts of recompilation.

Such problems would cause people to avoid using the exception specification mechanism or else subvert it [Koenig, 1990].”

Thus we decided to support run-time checking only and leave static checking to separate tools.

“An equivalent problem occurs when dynamic checking is used. In that case, however, the problem can be handled using the exception grouping mechanism presented in §16.4. A naive use of the exception handling mechanism would leave a new exception added to subsystem Y uncaught or converted into a call to unexpected() by some explicitly-called interface. However, a well-defined subsystem Y would have all its exceptions derived from a class Yexception. For example

class newYexception : public Yexception { /* ... */ };

This implies that a function declared

void f() throw (Xexception, Yexception, IOexception);

would handle a newYexception by passing it to callers of f()”.

For a further discussion see [2nd,§9].

In 1995, we found a scheme that allows some static checking of exception specifications and improved code generation without causing the problems described above. Consequently, exception specifications are now checked so that pointer to function assignments, initializations, and virtual function overriding cannot lead to violations. Some unexpected exceptions can still occur, and those are caught at run time as ever.

16.9.1 Implementation Issues

As ever, efficiency was a major concern. It was obvious that one could design an exception handling mechanism that could only be implemented with significant direct overhead in the function-calling sequences or indirectly through optimizations that were prevented by the possibility of exceptions. It appears that these concerns were successfully addressed so that in theory at least, the C++ exception handling mechanism can be implemented without any time overhead to a program that doesn’t throw an exception. An implementation can arrange that all run-time cost is incurred when an exception is thrown [Koenig, 1990]. It is also possible to limit space overhead, but it is hard to simultaneously avoid run-time overhead and an increase in code size. Several implementations now support exceptions so the tradeoffs will become clear; see for example [Cameron, 1992].

Curiously, exception handling doesn’t affect the object layout model to any real extent. It is necessary to represent a type at run time to communicate between a throw point and a handler. However, it appears that can be done by a special-purpose mechanism that doesn’t affect objects in general. Alternatively, the data structures supporting run-time type identification (§14.2.6) can be used. A much more critical point is that keeping track of the exact lifetimes of every automatic object becomes essential. Straightforward implementations of that can lead to some code bloat even where the number of added instructions actually executed is low.

My ideal implementation technique derives from work done with Clu and Modula-2+ [Rovner,1986] implementations. The fundamental idea is to lay down a table of code address ranges that corresponds to the state of the computation as relates to exception handling. For each range, the destructors that need to be called and the handlers that can be invoked are recorded. When an exception is thrown the exception handling mechanism compares the program counter to the addresses in the range table. If the program counter is in a range found in the range table, the appropriate actions are taken; otherwise the stack is unwound and the program counter from the calling function is looked up in the range table.

16.10 Invariants

Being a relatively new, evolving, yet heavily used language, C++ attracts more than its share of suggested improvements and extensions. In particular, every feature of every language that is fashionable somewhere will eventually be proposed for C++. Bertrand Meyer popularized the old idea of preconditions and postconditions and provided direct language support for it in Eiffel [Meyer, 1988]. Naturally, direct language support was suggested for C++.

Segments of the C community have always relied heavily on the assert() macro, but there has been no good way of reporting a violation of some assertion at run time. Exceptions provided such a way, and templates provided a way of avoiding reliance on macros. For example, one can write an Assert() template that mimics the C assert() macro:

template<class T, class X> inline void Assert(T expr,X x)
{
    if (!NDEBUG)
        if (!expr) throw x;
}

will throw exception x if expr is false and we have not turned off checking by setting NDEBUG. For example:

class Bad_f_arg { };

void f(String& s, int i)
{
    Assert(0<=i && i<s.size(),Bad_f_arg());
    // ...
}

This is the least-structured variant of such techniques. I personally prefer defining invariants for classes as member functions rather than using assertions directly. For example:

void String::check()
{
    Assert(p
           && 0<=sz
           && sz<TOO_LARGE
           && p[sz-l]==0 , Invariant);
}

The ease with which assertions and invariants can be defined and used within the existing C++ language has minimized the clamor for extensions that specifically support program verification features. Consequently, most of the effort related to such techniques has gone into suggestions for standardizing techniques [Gautron,1992], much more ambitious verification systems [Lea, 1990], or simple use within the existing framework.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset