"The (B)Leading Edge"

Reflections on Exceptions and the STL

by

Jack Reeves

©The C++ Report

This month's column is a reflection and some reconsideration of aspects from my two previous columns.

More on "Exceptions and Debugging"

After my column on Exceptions and Debugging[1], I received several comments from readers. One example:

Rob Jordan wrote (in part):

...

The technique you describe involves an exception being thrown from the site of an assertion failure (or strictly speaking from a _logic_error() function invoked at that site.

If the exception is caught in main(), then the stack is unwound on entry to the handler, and there will be no useful information in the core file relating to the state of the system when the assertion failed. True, you provide a technique for gathering the list of functions passed through as the stack unwinds, but no variable information is preserved.

If I leave the exception unhandled, then the core file retains the state of the system at the time the assertion failed, which could be useful for discovering the reason for the failed assertion. Admittedly, the system doesn't exit cleanly, and it isn't straighforward to log the cause of the error (I have resorted to logging from the _logic_error() function itself, and/or an installed terminate_handler() function). But I feel that it is more important to maximise information in the core file, at the expense of perhaps more detailed error logging.

By the way, I am writing an application, not providing class libraries for others to use, and I can understand that different factors may apply if one is providing a service for others to use.

Have I missed the point of your article? Do you agree with that more information can be retained in the core file if the exception is left unhandled?

My initial reply to Mr. Jordan was to note that he is absolutely correct about both the lack of information in the core file if the exception is caught in main(), as well as about the comparative lack of information provided by the stack trace-back mechanism that I proposed. Further, I noted that debugging is difficult enough, and recommended that if he finds it useful to have a core dump produced upon an error, then letting an exception propagate uncaught out of main() is almost certainly one way to do that. I did note that in my own work, customers tend to treat core dumps as major bugs, no matter what caused them. Therefore, our customer support department automatically categorizes any problem that causes a core dump in the field as priority A. As a result, the software department has a policy of not releasing any software with assert's enabled. As we migrated to C++, a similar policy was proposed for exceptions. By having main() catch all exceptions, I can field software that (a) meets the companies criteria for no deliberate core dumps, (b) has at least a tiny bit of grace when abnormally exiting, and (c) provides some useful information in the error log.

Mr. Jordan's note started me thinking, however: were there not aspects of my proposal that could be improved upon. In particular, while it is obvious that what makes sense in a debugging environment is not what you want to release to the customer, as a general rule you want to make adding and removing debugging code as simple as possible. My proposal was aimed at solving several different problems which occur in my current development environment, but it was also based upon some observations (about the use of assert macros, or more correctly the lack of use) that, while true, were not necessarily a situation that should be considered good practice. Then I received the following:

Kuha Jorma wrote:

To: Doug Scmidt, Editor, C++ Report,

"Letters to the Editor" - column.

I disagree with the idea of using exceptions to implement assertions as proposed in Jack Reeves' article "Exceptions and Debugging" published in C++-report Vol 8 /No 10 1996, because of three reasons:

1) His examples for using assertions were not consistent nor sufficient.

The three categories for usage of assertions according to Reeves were:

- Rudimentary error checking (failures when allocating memory or opening a file).

- Checking input parameters for a function.

- A stopper for code paths that should not be entered.

and he argued that in all these cases the cause of the error was out of his control, thus promoting usage of exceptions.

Among others, Steve Maguire explicitly states error checking with assertions as incorrect ( "Writing Solid Code", Microsoft Press, 1993). They should not be used to test error conditions that may or will show up in the final product.

Also, Reeves does not mention class invariants, loop invariants nor pre- and postconditions, all of which are very typical applications for assertions (see Bertrand Meyer: "Object-Oriented Software Construction", Prentice-Hall, 1988). Loop invariants and postconditions also indicate a programming error very close to the actual assertion-statement itself. If a user of a function gives improper arguments for it, it is noticed in precondition check, and the old assert-macro is a perfectly valid technique for implementing it.

2) When detecting an assertion-failure, program execution should be halted as soon as possible.

If, for example, the cause for the illegal condition is writing garbage data over the function stack area, attempts to throw exceptions result in calling the terminate()-function (in the best case), so the exception will never be handled.

Moreover, if we use assertions to verify the invariant of the object also in the beginning of its destructor, and if a local object's destructor now throws an exception while searching for the handler of the original exception, the original exception will again never be handled (but the terminate()-function will be called). So exceptions cannot be used to inform about these erroneous conditions at all.

3) The non-exception based assert-macro can be used also when there is not enough memory for storing the assertion-messages in strings (for example, by redefining the assert-macro to call a dummy function when its expression is false, by placing a breakpoint with a debugger there, and by stepping back to next instruction after the failed assertion) - this can be very important in embedded systems. This is not possible with exception based assertions without special debuggers, as the information in stack is destroyed during stack unwinding.

It was a surprise for me that the Draft Standard language support library throws "logical exceptions" such as "out_of_range" for example when accessing template class basic_string past its end. I believe this approach has fundamental problems and should be further discussed.

Jorma Kuha,

Nokia Research Center,

Mr. Kuha raises several important points which caused me to think further about the role of the assert macro (and its default behavior) versus using exceptions for debugging. As a result, I spent some time reviewing the entire concept that I had presented in [1]. As a result of Mr. Kuha's observations, I now feel that I have a much better understanding of what I was really trying to accomplish. As a result of that understanding, I made one major change, and several minor clarifications to the proposal. I am now using the "new" version on my current project. It seemed appropriate to address Mr. Kuha's points in some detail as way of clarifying and explaining my changes.

First, let me once again note that this column has the name that it does because much of what I am proposing is experimental -- at best. These are techniques that I and my colleagues are working out and applying to current projects as we go along. While my management is extremely supportive (thanks Dan and Eric!), I do not have carte blanch to make wholesale changes to our coding practices. This is OK by me as I consider it a critical reality check. All of the ideas that I have proposed have been reviewed by a number of other good developers, and thought well enough of to be adopted as part of our project coding guidelines. Still, the actual experience level that we have with some of these ideas is hardly more extensive than a typical upper level college project. They seem to be working so far, but I will feel much better when there have been a million lines of code and several projects developed using them. As always, if you try these techniques and discover problems or refinements that make them better, I would like to hear about it. If you find that they work for you, I wouldn't mind hearing about that either. I will continue to publish (with the editor's permission) letters which describe such experience.

Next, let me make it clear that I am not opposed to using assertions in my code. On the contrary, I have argued strongly on past projects that they should be more widely used. I certainly did not mean to indicate that the three examples of assertions that I showed ((a) rudimentary error checking; (b) checking input parameters; and (c) a stopper for code paths that should not be entered) were the only things that assertions could or should be used for. These were just the only examples of assertions that I found in a review of some old code. As noted by Mr. Kuha, class invariants, loop invariants, pre- and post- conditions are all cases for assertions.

One problem that I see, even in my own software, is that assertions are not used nearly as much as they should be. Certainly, I do not code class invariants, or loop invariants, on a regular basis, and I do not know of anyone else who does. This is not a good thing, for I know that code liberally sprinkled with assertions tends to be easier to test and debug than code that does not use assertions. Nevertheless, Mr. Kuha and I seem to differ in two areas: (a) what exactly should be an assertion, and (b) what should happen if an assertion fails. The two are interrelated, but for now let's consider them separately.

I could not agree more with the statement attributed to Steve Maquire. Assertions are not for error checking. Rather, I view assertions as an adjunct to testing and debugging. When you write an assertion, you should just be making explicit some condition that should always hold. In other words, if the code is correct, an assertion should never fail. Obviously, we write assertions because we might not get the code correct the first time, but the key point is that an assertion failure should not happen in a debugged program. The problem is: deciding what is and what is not "a condition that should always hold" involves a certain degree of judgment and often depends upon the context that the software is being developed in and how it is intended to be used. For example, in certain circumstances it might be reasonable to assert that a map<>.find() call will never fail; in other words, the existence of 'x' as a key in the map is part of the function's precondition. Being unable to easily test this directly, we could assert the success of the find as part of the function's postcondition:

it = m.find(x); assert(it != m.end());

In other cases, this assertion would be incorrect. Even though it might be rare that 'x' would not be in the map, it still might be possible, and the function would be expected to deal with such an occurrence. This differentiation between "never" and "rarely" is fundamental to using assertions correctly.

One particular area where reality tramples rudely upon theory is the question of what to do about invalid function arguments. The position apparently taken by Mr. Kuha (with attribution to Bertrand Meyer) is that argument validity is part of the function's precondition: "If a user of a function gives improper arguments for it, it is noticed in precondition check, and the old assert-macro is a perfectly valid technique for implementing it." On the other hand there is Steve Maguire's point that "error checking with assertions is incorrect and should not be used to test error conditions that may or will show up in the final product." The open question is whether a bad argument is an error condition that may or will show up in the final product, or just a failure of the precondition for the function?

How you answer that question probably depends upon where you sit in the development hierarchy. From the outside of an interface, getting a function's arguments correct is just a precondition to having the software work correctly. From the inside it is likely to be a different story. These days, software is developed by large, diverse teams. At some point many programmers stop seeing themselves as just coders and start feeling like they are software developers with a product that they are responsible for. This feeling is often heightened in C++ development where we strive for some level of reusability from the outset. The client may be the guy in the next cubical, the group down the hall, the division across the country, or another company altogether. The farther the software has to go, the more concerned we become with making sure that "what goes out the door" is really the product we want it to be.

Everybody understands the idea of a "debugging" version of the software, and a "released" version. Assert-macros are specifically designed to cater to the difference. They can be rendered impotent by compiling with the preprocessor macro NDEBUG defined. This makes it possible to write assertions in our code, test it to our hearts content, and then remove our "debugging" code by just recompiling. But what happens if we have been using assert-macros to check function preconditions in a software module that gets "released" to the client. The software works, so the internal assertions are theoretically redundant and can safely be compiled out, but this also removes the validity checks on the function arguments. In a theoretical sense, this is not a problem. Since the requirements for using the library are clear (we hope), any attempt to invoke one of its functions with invalid arguments results in undefined behavior, which is the client's problem.

In the real world, unchecked function arguments are a very real problem. Hopefully, you will never be called on the carpet by your boss, who in turn is on the hot seat with his boss, who is getting complaints from the other division about core dumps in their new (late) application that are being traced to your code. The fact that you are pretty sure that the problem is in their code will not help you&. If you are lucky, you will get to drop everything you are doing and help the other guys debug their software. If you are not so lucky you will be given a few hours to pack and catch an airplane to some place you do not want to go. This only happens to you once before you learn that there is a big difference between writing an assertion for something you have some control over, and a precondition check of a function argument in a released module.

Even if you have "released" your software with the assertions enabled, you may still have problems. As I noted in the column[1], the error message from an assert-macro just gives the location in your code where the assertion failed. This will not do the client any good whatsoever if they do not have access to your source code. So, let's assume (for now) that your "released" software also includes the source code. Now at least the client can look at the code that detected the error. Presumably, with this much information, they will be able to make the connection between the argument that caused an assertion failure, and their own code -- but you still might get phone calls (you do put your phone number in the header of your source files, don't you).

If you (or your company) prefers to release software that has its internal debugging code disabled, that does not come with source code, and that does not have the developers personal phone number attached to it, then you probably have no problem agreeing that invalid function arguments should be treated as errors that may (and probably will) show up in released code. This may not matter if all your software is used in-house, and all of it is "released" together, but the reality (or at least the goal) in many C++ shops is reusable component development with their own release cycles. It therefore follows that the assert-macro is not the tool of choice for checking function arguments.

Until recently, this was all beside the point. No matter how you checked them, the real problem was what to do about it when you detected a bad argument. One choice was to add the problem to the regular error handling approaches -- status returns, global error codes, etc. This generally seemed like a waste of time since the error could be expected to be detected and fixed during testing and debugging anyway. One way to speed this process up was to call abort(). Given this choice, the assert-macro was as good as any other way to code the test. That changes in Standard C++. Exceptions provide a uniform way for a C++ function to signal the detection of an error. It does not matter whether the error is a runtime error -- one of those things that may or will show up in the finished product -- or a logic error which clearly should not be there. The observed reality (in my corner of the universe at least) is that C++ programmers prefer to throw exceptions when they detect such "precondition" errors.

As I discussed in my column on Guidelines for Throwing Exceptions [6], it is a good idea to try to provide the client with some way to disable certain error checks. That guideline was specifically aimed at precondition checks. You want to let the client make the decision as to when those checks can be safely disabled as part of his debugging process, not yours. Doing this in C++ usually involves the preprocessor since the language does not directly support the concept of assertions. I have become quite fond of the idea of implementing the public interface of a class as inline functions that do the precondition checks and then call private functions that actually do the work. This way, if the precondition checks are implemented by macros (like assert) they can be enabled or disabled selectively wherever they are used.

Another area where theory and reality clash is in the concept of "released" versus "debugged" software. In theory, released software has been thoroughly debugged. In practice, this is often painfully far from the truth. In such cases, it often seems like it would make sense to "release" the software with at least some of the debugging code still enabled. Besides the obvious pragmatic issue of how to disable some assert-macros and not others, a psychological issue also comes into play. If you are a user, and you get a software crash, you know you have a problem. You may get upset, worried, even angry. You may call the company support line and yell at them. You may even swear to never use their software again until they get it right. Nevertheless, you "know" that totally bug free software is "impossible". Suppose you get a system crash with a message that says something like "Assertion failure on line 578 in AbcDef.c". You may have all of the same reactions you did before, but you will also probably feel a touch of disgust that some programmer got lazy and threw in an assert macro instead of writing a proper error handler. After all, assertion failures are not suppose to happen in released software, right? As a result, many companies take the position that their shipping software does not contain "live" assertions. The code may not be bug free, but they prefer to pretend that it is rather than have users think they didn't test all of the assertions.

Again, using exceptions provides a way out of this dilemma. A test that throws an exception can be left in the code. In simple cases, the exception can be ignored and the behavior is similar to the default behavior of the assert-macro. In more complex cases, the exception gets caught, and the program attempts to exit somewhat gracefully. As I discussed previously, with exceptions the application gets to decide, both what it is going to do about the error (it might even decide to continue execution), and what (if any) message gets displayed to the user. This last point is much more important than I realized at first. With the assert-macro, you get whatever error message the assert-macro gives you. I have already discussed how that might not even be useful for debugging without access to the source code. When using exceptions, the message in the exception (which is probably very similar to the typical assert-macro message) can be written to an error log somewhere, but the user can be given a much more "ergonomic" version. I am not advocating writing elaborate exception handling code for logic errors -- the problem still has to be tracked down and fixed -- but a reasonably intelligent and somewhat useful error message can help considerably in making even buggy software look like it was developed by professionals.

A lot of these issues may not have been thought about by the ordinary programmer in so many words or less. Probably the only thing most C++ programmers realize is that if they throw an exception, someone else gets to decide what to do about it. Whether well thought out or not, such conclusions become part of the psychological profile of a project. This was, in essence, what I saying when I observed that I could no longer find any use of assert macros on the C++ projects that I was working on. This may be the current reality in my environment, but is it the reality that we wish to have?

I had noted the problems with using the assert-macro for testing function arguments and the problems with leaving assert-macros enabled in released code. I observed the complete lack of assert-macros in a considerable quantity of C++ code that I had access to. On the other hand, the project code was riddled with throws, many of which made no sense at all. I concluded that C++ programmers do not like to use the assert macro. This was probably an incorrect assumption on my part. It is probably safer to say that the project that I was working on had gotten overly enamored with whole idea of using exceptions without having any clear guidelines of how to use them, and was far less concerned about questions of testability than it should have been. Being concerned with problems of testability and debugging, I set out to cobble together a scheme that used exceptions for signaling logic errors instead of using the assert-macro with its automatic core dump. In hindsight, I now realize that this only partially addressed the problem.

The assert-macro, even in its straight C form, is perfectly valid for what it is intended for: as a documentation and debugging aid. Given a policy of disabling assert macros in released code, the real reason they were not being used was that nobody was taking the time to write them. This is not (strictly speaking) a C++ problem, but I note in passing that it may be one more aspect of Tom Cargil's "False sense of security"[4] that comes when first using exceptions.

Having regained my respect for assertions, and for the assert-macro as a way to provide them in C++, the question still remained: is the default behavior of the assert-macro the appropriate behavior. At one point, I wondered why Standard C++ did not provide a standard exception for an assertion failure (Ada does), and why the assert-macro was not defined in Standard C++ to throw that exception by default. I now realize that the committee was not about to change something in the Standard C Library that was not obviously broke. Besides, as Stroustrup mentions in D&E [7], programmers can always define their own version of Assert which throws exceptions. I have done that, but I also decided to change the assert-macro to throw exceptions by default.

Mr. Kuha makes several points against this idea, and his concerns deserve answers. I have already tackled his first point: there are lots of places where assertions make sense and the default assert-macro that aborts the program can be used, but I do not think that testing function arguments is one of them.

His second point is that in case of an assertion failure, program execution should be halted as soon as possible. I disagree. He is certainly correct that there are classes of error conditions (such as writing garbage into the stack area) that will thwart an attempt to handle an exception. Such cases call terminate() so one could say that the exception based approach degenerates to the standard assert-macro behavior. The one difference, which I presume is Mr. Kuha's point, is that I had the error message being produced when the exception was handled, which will not happen if terminate() is called, whereas the assert-macro writes out the error message before it calls abort(). This is true. I considered addressing this problem by installing a user defined terminate-handler of the form

void a_terminate_handler()

try {

throw; // rethrow exception to get a handle to it

} catch (exception& ex) {

cerr << ex.what() << endl;

abort();

} catch (...) {

abort();

}

This seems to work, but I have no idea how portable it is. I would have to say from my reading of the DWP [3] that the semantics of throwing an exception in a terminate handler are undefined. I have changed this aspect of my proposal, slightly.

Mr. Kuha's also talks about using assertions to verify class invariants at the beginning of a destructor. This is a more interesting argument. Mr. Kuha is correct that throwing exceptions for an assertion failure in a destructor is likely to just end up calling terminate(). The overall problem of trying to throw exceptions from destructors has been discussed in some detail before[5] and precisely the same question applies to assertions as to any other error checking in destructors: what is the point? A destructor is not an ordinary function. Strictly speaking, the object exists until the body of the destructor finishes executing. Therefore, a destructor may be called upon to perform ordinary operations on the object. Nevertheless, the fundamental task of the destructor is to destroy the object. The chances are fairly good that if a class invariant does not hold at the beginning of a destructor, the destructor is being called on behalf of an exception that is the result of whatever failure destroyed the class invariant. The only valid preconditions to a destructor are those aspects which would keep the destructor from being able to destroy the object. My feelings in this regard have not changed (see [5]): you should make sure your destructors will execute.

It is fairly easy to identify situations in which program execution can be halted immediately (such as debugging); I find it much more difficult to identify application domains where execution should be halted immediately. On the contrary, those application domains that come to mind which require such correct behavior that assertions are routinely left enabled are often domains where total program crashes are completely intolerable. Embedded systems are a good example. Most embedded systems have physical resources under their control. Logic errors in the program could lead to hardware failures or safety hazards. Therefore, it is imperative that such systems check for logic errors and immediately shut down if they are detected. On the other hand, a total application crash in such a system could lead to exactly the same problems of hardware failure or risk. Almost invariably, there are some ways to exit a system that are more graceful than others.% If immediate program exit is acceptable, then the exception thrown by an assertion failure can be ignored. If a more graceful exit is necessary, then an exception allows that possibility.

Mr. Kuha's third point is more of an argument in favor of using a macro to check assertions than it is an argument against using exceptions. All of the things he suggests are equally possible with exception based assertions. In fact, you do not even have to redefine the macro, just replace the helper function that does the actual throw. Originally, these helper functions were just a convenience -- they gave me a place to put together the message string without having to continually do it inline. Now, the helper functions are a required part of the mechanism. My exception throwing guidelines recommend that exceptions should never be thrown directly, but always should be thrown by such helper functions. The helper functions provide hooks into the mechanism for debugging and other special situations.

In addition to always using helper functions to implement the assertion failure policy, if the assertion check is itself a macro which can be redefined, then all of Mr. Kuha's third point are as possible using exception based assertions as they are with the current mechanism.

Finally, I sympathize with Mr. Kuha's concerns about the Draft Standard Library throwing exceptions for things like "out_of_range" in basic_string. My own concerns were the opposite of his: I felt the library did not throw exceptions in a lot of places where it should. In either case, it is much too late to expect any changes to the Draft Standard Library. My advise to Mr. Kuha in this case is not to worry about it. There are really only a very few places in the Draft Standard Library where exceptions such as "out_of_range" are explicitly called for. I believe that in all of those cases, there are other versions of the functions available which do not specify an "out_of_range" exception. For the most part, the committee has left the error behavior of the library undefined.

Originally, I was concerned that this would allow vendors to provide versions of the Standard Library with no error checking. I now realize that this doesn't give C++ library vendors enough credit. I still have concerns, but I am reasonably sure that most vendors will provide versions of the Standard Library that give clients a choice of

- no error checking

- assert-macro style error checking

- exception based error checking

Some may additionally provide hooks to allow the user to supply their own error handling policy.

After all this rambling, the bottom line is "what have I changed in my proposal."

A. The primary change was one of attitude. I have reasserted :-) the importance of assertions in the code. I considered hijacking the existing assert macro, either by redefining it or by replacing its' helper function. Both choices are technically out-of-bounds, i.e. both "assert" (a macro defined in the Standard C Library) and "__assert" (its typical helper function) are identifiers reserved to the implementation by the (draft) Standard. This left me with something of a dilemma. If I wanted to encourage more use of assertions, then it seemed like a good idea to allow the standard form, no matter what other forms I might provide. On the other hand, I did not want the standard assert-macro behavior.

After going back and forth for days, I finally decided that pragmatism and the possibility of fewer errors outweighed the political correctness of leaving the assert macro strictly alone. The following is non-portable, undefined behavior according to the (draft) Standard, and is probably fattening as well. Nevertheless, I present it as an option.

A.1 - I replaced the __assert() helper function with a C++ specific version. The default behavior of this function is to write the error message to cerr, and throw an exception. This behavior can be overridden by supplying a user defined assert_handler function.

A.2 - The user can supply an assert_handler function via a call to set_assert_handler(). The assert_handler is passed the exception to be thrown as an argument. A user defined assert_handler() can throw an exception (usually it will throw a new exception; it can throw its argument, but unless the assert_handler has used RTTI to determine the actual type of the argument, throwing it will slice it to the base class exception). An assert_handler() can terminate the program (calling terminate() is the recommended way to do this). Finally, if the assert_handler() returns the original exception is thrown.

B. I defined a new macro called check. It is similar to the assert macro, but differs in three important ways:

i. The check-macro is not disabled when NDEBUG is defined.

ii. The check-macro takes an additional argument of the function name.

iii. The default behavior for the check-macro is to just throw the exception without first writing the error message to cerr.

A user defined assert_handler can override the default behavior of the check macro.

C. I defined a series of other macros that throw specific exceptions:

assert_length check_length

assert_range check_range

assert_argument check_argument

assert_domain check_domain

Each of the "assert_xxx" versions has a corresponding preprocessor macro which will disable it. All "assert" macros are disabled when NDEBUG is defined; the "check" macros remain active.

D. Since the name of the macro helper function is bound at the point of the macro's invocation (instead of at the point of definition), the helper function can be overridden within nested scopes by a scope specific version. Class developers are encouraged to provide class specific versions of the helper functions. These class specific helper functions can add more detail to the error message, or throw class specific exceptions. They also can invoke a class specific user defined error handler. If the class does not allow a user defined error handler, or if one is not provided, the helper function should check for and invoke any global assert_handler(). In many domain libraries, class specific helper functions log the error to an common error log before throwing the exception. Listing 1 provides definitions of the macros. Listing 2 provides a couple of example definitions for the helper functions.

This scheme is not the most elegant or the most flexible, but it does have several advantages.

1. The default behavior of the assert-macro is essentially unchanged -- you get a message and the program aborts. The caveat is that the exception must be allowed to propagate out of main. An application which catches all exceptions at some point will prevent the program abort. This is a deliberate choice.

2. The assert_handler scheme follows the same form as terminate_handler and unexpected_handler. It allows the default behavior to be overridden. This can be used during debugging to trap to the debugger before the exception is thrown, or to simply abort the program and receive the core dump. It can be used in released applications to direct the error message somewhere other than to cerr.

A final note: my proposal to have debugging objects in functions generate a trace of their destruction during an exception stack unwind has not changed. I would like to clarify that this proposal was intended for released applications, not those in debug. The intent is to get something more than just a single error message if the program unexpectedly terminates. As both Mr. Jordan and Mr. Kuha noted, a core file is far more useful; I just happen to think that core dumps in the client's environment are really tacky and should be avoided if at all possible.

I note that my philosophy on error handling was formulated many years ago by Kernighan and Plauger's classic book "Software Tools". In it they build an editor. On the topic of error recovery they note that an editor "cannot just throw up its hands and quit when a user enters an erroneous command. It must recover gracefully." Over the years I have generalized this philosophy to all software and all errors. Ideally, every program should recover gracefully. In the real world, most programs do not need that level of reliability. A lesser goal is to at least exit gracefully. Before exceptions, this also usually took more work than it was worth. Using exceptions, it seems to be fairly easy, and I am now looking forward to regularly achieving at least my secondary goal.

One issue I am still debating is what exception should assert and check throw upon failure. Right now, they both just throw logic_error. The other two options that I have considered are (i) throw a specific exception assertion_failure derived from logic_error, or (ii) throw an exception such as bad_assertion which is derived directly from exception. Having assert-macros throw a unique exception means that they can be dealt with in a individual manner. In particular, if bad_assertion were used then a main program with just a catch(logic_error&) clause would allow the exception from an assertion failure to propagate out of main(), invoking terminate(). I still lean towards catching everything in main(), but having bad_assertion is still appealing because it elevates assertions to an equivalent level as such built-in language constructs as new (which can throw bad_alloc), typeid (which can throw bad_typeid), and dynamic_cast (which can throw bad_cast). On the other hand, it would mean that an exception specification of throw(logic_error) would not pass assertion failures.

Any reader who has a reasoned opinion either way on this topic is invited to send it to me.

 

References

1. Jack Reeves, "The (B)Leading Edge: Exceptions and Debugging", C++ Report, Vol. 8, No. 10, November/December 1996.

2. Jack Reeves, "The (B)Leading Edge: STL Gotcha's", C++ Report, Vol. 9, No. 1, January 1997.

3. "Working Paper for Draft Proposed International Standard for Information Systems -- Programming Language C++", December 1996.

4. Tom Cargill, "Exception handling: A false sense of security", C++ Report, Vol. 6, No. 9, November-December 1994.

5. Jack Reeves, "Using Exceptions Effectively: Coping with Exceptions", C++ Report, Vol. 8, No. 2, March 1996.

6. Jack Reeves, "The (B)Leading Edge: Exceptions and Standard C++", C++ Report, Vol. 8, No. 5, May 1996.

7. Bjarne Stroustrup, The Design and Evolution of C++, Addison-Wesley, 1994.

SIDEBAR

Object-oriented Software Construction

by Betrand Meyer

In every technical field, there are a set of "standard" references. If you intend to be a practitioner in that field, you will be expected to have at least a passing familiarity with these works. In software development, such names as Dijkstra, Wirth, Kernighan, Plauger, Knuth, DeMarco, Yourdon, and Booch immediately come to mind. In the C++ community we have Stroustrup, Meyers, Murray, Cargil, and Lippman, to name a few. On the other hand, there are often a number of "classic" works which are well known in certain circles, but are not part of the standard lexicon of everyday practitioners. Do the names Bohm, Jacopini, or Parnas ring any bells with you? Bertrand Meyer's "Object-oriented Software Construction" is one of the classic texts in object oriented software development, but it is not one of the "standard" texts. This is too bad.

Ignoring for the moment that all the software in the book is written in Eiffel (Meyer's own O-O programming language), there is a tremendous amount of useful information in Meyer's book that is directly applicable to C++ programming. Assertions are a vitally important part of Meyer's approach to error free software construction. Even though C++ does not provide the direct support for assertions that Eiffel does, the principals are equally applicable and in many cases can be simulated by using some variant of the C assert macro (as demonstrated by some of the examples in Steve Maquire's book that is also mentioned by Mr. Kuha). A key difference between Eiffel assertions and the assert macro in C is that Eiffel generates exceptions when assertions are violated. Exceptions in Eiffel are not like exceptions in C++, however -- you can not "handle" an Eiffel exception, you can only try to correct the problem and retry the operation, or pass the exception on. As a result, assertions in Eiffel usually terminate the program -- with an error message -- quite similarly to what the assert macro does in C.

Don't be fooled. Eiffel has far more in common with the proposal I have outlined above than with C's assert macro. Eiffel generates exceptions for assertion failures for precisely the same reasons that I decided they should be used for logic errors in my C++ code -- in the real world, programs often have to remain "in control", even if the only rational thing for them to do is terminate. You can always ignore an exception, doing a graceful shutdown after a call to abort() is much more difficult.

It has been awhile since I had my copy of "Object-oriented Software Construction" off the shelf. I would like to thank Mr. Kuha for reminding me to pull it down and review it.

Listing 1.

Alternate check/assert macros

//

// check macros

//

#undef check

#undef check_length

#undef check_range

#undef check_argument

#undef check_domain

void raise_logic_error(const char*, const char*);

void raise_length_error(const char*, const char*);

void raise_out_of_range(const char*, const char*);

void raise_invalid_argument(const char*, const char*);

void raise_domain_error(const char*, const char*);

#define check(cond, func) \

((cond) ? (void)0 : \

raise_logic_error("check (" #cond ") failed", func))

#define check_length(cond, func) \

((cond) ? (void)0 : \

raise_length_error("check (" #cond ") failed", func))

#define check_range(cond, func) \

((cond) ? (void)0 : \

raise_out_of_range("check (" #cond ") failed", func))

#define check_argument(cond, func) \

((cond) ? (void)0 : \

raise_invalid_argument("check (" #cond ") failed", func))

#define check_domain(cond, func) \

((cond) ? (void)0 : \

raise_domain_error("check (" #cond ") failed", func))

//

// assert macros

//

#undef assert

#undef assert_length

#undef assert_range

#undef assert_argument

#undef assert_domain

#ifdef NDEBUG

#define assert(cond) \

((void)0)

#define assert_length(cond, func) \

((void)func)

#define assert_range(cond, func) \

((void)func)

#define assert_argument(cond, func) \

((void)func)

#define assert_domain(cond, func) \

((void)func)

#else

void __assert(const char*, const char*, int);

#define assert(cond) \

((cond) ? (void)0 : \

__assert("assert (" #cond ") failed", __FILE__, __LINE__))

#ifdef NO_LENGTH_ASSERT

#define assert_length(cond, func) \

((void)func)

#else

#define assert_length(cond, func) \

((cond) ? (void)0 : \

raise_length_error("assert (" #cond ") failed", func))

#endif

#ifdef NO_RANGE_ASSERT

#define assert_range(cond, func) \

((void)func)

#else

#define assert_range(cond, func) \

((cond) ? (void)0 : \

raise_out_of_range("assert (" #cond ") failed", func))

#endif

#ifdef NO_ARGUMENT_ASSERT

#define assert_argument(cond, func) \

((void)0)

#else

#define assert_argument(cond, func) \

((cond) ? (void)0 : \

raise_invalid_argument("assert (" #cond ") failed", func))

#endif

#ifdef NO_DOMAIN_ASSERT

#define assert_domain(cond, func) \

((void)func)

#else

#define assert_domain(cond, func) \

((cond) ? (void)0 : \

raise_domain_error("assert (" #cond ") failed", func))

#endif

#endif

//

// assert_handler

//

typedef void (*assert_handler)(exception&);

assert_handler set_assert_handler(assert_handler);

 

Listing 2

Assert macro helpers

Example Implementations

static const char* what_base = "Exception: () thrown from ";

static string

make_what_str(const string& ex,

const string& why,

const string& where)

{

string str = what_base;

str.insert(what_str.find("()"), ex);

str.insert(what_str.find("()")+1, why);

str.append(where);

return str;

}

void

__assert(const char* cond, const char* file, int line)

{

ostrstream os;

os << line;

logic_error ex(

what_str(

"std::logic_error",

cond,

string("file \"") + file + "\", line "

+ os.str()

)

);

assert_handler ah = set_assert_handler(null);

if (ah) {

set_assert_handler(ah); // reset it

(*ah)(ex);

} else {

cerr << what_arg << endl;

}

throw ex;

}

void

raise_logic_error(const char* cond, const char* func)

{

logic_error ex(what_str("std::logic_error", cond, func));

assert_handler ah = set_assert_handler(null);

if (ah) {

set_assert_handler(ah); // reset it

(*ah)(ex);

}

throw ex;

}