10.7 Exceptions


In reactive classes, exception handling is straightforward exceptions are signals (associated with events) specified in the class state model that result in transitions being taken and actions being executed. In nonreactive classes, the specification means is less clear. The minimum requirements are to identify the exceptions raised by the class and the exceptions handled by the class.

Exception handling is a powerful addition to programming languages. Language-based exception handling provides two primary benefits. The first is that exceptions cannot be ignored. The C idiom for exception handling is to pass back a return value from a function, but this is generally ignored by the clients of the service. When was the last time you saw the return value for print checked?

The correct usage for the C fopen function is something like this

 FILE *fp; if ( (fp = fopen("filename", "w")) == NULL) {     /* do some corrective action */     exit(1); /* pass error indicator up a level */     }; 

Many programmers, to their credit, always do just that. However, there is no enforcement that the errors be identified or handled. It is up to the programmer and the code peer review process to ensure this is done. With exceptions, the error condition cannot be ignored. Unhandled exceptions are passed to each preceding caller until they are handled a process called unwinding the stack. The terminate-on-exception approach has been successfully applied to programs in Ada and C++ for many years.

The other benefit of exception handling is that it separates the exception handling itself from the normal execution path. This simplifies both the normal processing code and the exception handling code. For example, consider the following standard C code segment:

 if ( (fp = ftest1(x,y,z))) == NULL) {     /* do some corrective action */     printf("Failure on ftest1");     exit(1); /* pass error indicator up a level */     }; if (!ftest2()) {     /* do some corrective action */     printf("failure on ftest2");     exit(1);     }; if (ftest3() == 0) {     /* do some corrective action */     printf("failure on ftest3");     exit(1);     }; 

This is arguably more difficult to understand than the following code:

 // main code is simplified try {     ftest1(x,y,z);     ftest2();     ftest3(); } // exception handling code is simplified catch (test1Failure& t1) {     cout << "Failure on test1";     throw; // rethrow same exception as in code                above } catch (test2Failure& t2) {     cout << "Failure on test2";     throw; }; catch (test3Failure& t3) {     cout << Failure on test3";     throw; }; 

The second code segment separates the normal code processing from the exception processing, making both clearer.

Each operation should define the exceptions that it throws as well as the exceptions that it handles. There are reasons to avoid using formal C++ exceptions specifications [2] but the information should be captured nonetheless. Exceptions should never be used as an alternative way to terminate a function, in much the same way that a crowbar should not be used as an alternative key for your front door. Exceptions indicate that a serious fault requiring explicit handling has occurred.

Throwing exceptions is computationally expensive because the stack must be unwound and objects destroyed. The presence of exception handling in your code adds a small overhead to your executing code (usually around 3%) even when exceptions are not thrown. Most compiler vendors offer nonstandard library versions that don't throw exceptions and so this overhead can be avoided if exceptions are not used. Destructors should never throw exceptions or call operations that can throw exceptions nor should the constructors of exception classes throw exceptions.[11]

[11] In C++, if an exception is thrown while an unhandled exception is active, the program calls the internal function terminate() to exit the program. As the stack is unwound during exception handling, local objects are destroyed by calling their destructors. Thus destructors are called as part of the exception handling process. If a destructor is called because its object is being destroyed due to an exception, any exception it throws will terminate the program immediately.

Exception handling applies to operations (i.e., functions) and is a complicating factor in the design of algorithms. In my experience, writing correct programs (i.e., those that include complete and proper exception handling) is two to three times more difficult than writing code that merely "is supposed to work."[12]

[12] In contrast to prevailing opinion, I don't think this is solely due to my recently hitting 40 and the associated loss of neural cells.

Capturing the exception handling is fundamentally a part of the algorithm design and so can be represented along with the "normal" aspects of the algorithms. Exceptions can be explicitly shown as events on either statecharts or activity diagrams.

That still leaves two unanswered questions:

  • What exceptions should I catch?

  • What exceptions should I throw?

The general answer to the first question is that an operation should catch all exceptions that it has enough context to handle or that will make no sense to the current operation's caller.

The answer to the second is "all others." If an object does not have enough context to decide how to handle an exception, then its caller might. Perhaps the caller can retry a set of operations or execute an alternative algorithm.

At some point exception handling will run out of stack to unwind, so at some global level, an exception policy must be implemented. The actions at this level will depend on the severity of the exception, its impact on system safety, and the context of the system. In some cases, a severe error with safety ramifications should result in a system shutdown, because the system has a fail-safe state. Drill presses or robotic assembly systems normally shut down in the presence of faults because that is their fail-safe state. Other systems, such as medical monitoring systems, may continue by providing diminished functionality or reset and retry, because that is their safest course of action. Of course, some systems have no fail-safe state. For such systems, architectural means must be provided as an alternative to in-line fault correction.

Exceptions can be modeled in several ways in the UML. In the absence of state machines and activity diagrams, throw, try, and catch statements are simply added to the methods directly. This works well within a single thread, but fails thread boundaries. For reactive objects those with state machines the best way is simply to model the exceptions as events. If the event handling is asynchronous, then this approach will work across thread boundaries. However, virtually all state machines use FIFO queues for unhandled events. Since the UML does not assign priorities to events, exception events are treated as normal events and will be handled in a FIFO way. This may not be what is desired.

For activities, exceptions are modeled as interrupting edges applied to an interruptible segment of an activity diagram. This causes the activity to terminate and control flow proceeds along the interrupting edge.



Real Time UML. Advances in The UML for Real-Time Systems
Real Time UML: Advances in the UML for Real-Time Systems (3rd Edition)
ISBN: 0321160762
EAN: 2147483647
Year: 2003
Pages: 127

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net