D - Recursive exceptions
- Antti Sykari (41/41) Feb 11 2003 Someone mentioned that exceptions should be allowed to be thrown also
- Ken Carpenter (40/52) Feb 12 2003 In this case, you have presumably detected a condition that requires you...
- Antti Sykari (12/30) Feb 12 2003 Yeah, you can never know what happens - and that's more or less the
- Antti Sykari (122/132) Feb 12 2003 A more precise formulation of the problem, or an abstract, if you
- Daniel Yokomiso (35/76) Feb 12 2003 Hi,
Someone mentioned that exceptions should be allowed to be thrown also when an exception is active, that is, during the unwinding of stack. As we know, in C++ and probably in D, too, throwing exceptions during stack unwinding results in a catastrophe of some kind, probably abort(). So, what happens when an exception escapes a member function of a class, and leaves that object's invariant in a bad state? class BadlyBehaving { foo() { locked = true; // ... throw new Whatever(); // ... locked = false; } bit locked = false; invariant() { assert(!locked); } } f() { BadlyBehaving b; try { b.foo(); } catch (Whatever x) { // handle whatever } // but now b is in a bad state } now f() calls b.foo(), which causes an exception to be thrown, and when it exits the scope of BadlyBehaving.foo(), the class invariant is violated. Hence, InvariantException or similar should be thrown, but an exception is already being thrown so we have a problem. If two (or more) exceptions could be active at the same time, this would allow us to catch the invariant violation. Which is nice if you want to have some kind of exception safety guarantees. -Antti
Feb 11 2003
"Antti Sykari" <jsykari gamma.hut.fi> wrote in message news:86y94m0y4f.fsf hoastest1-8c.hoasnet.inet.fi...So, what happens when an exception escapes a member function of a class, and leaves that object's invariant in a bad state? class BadlyBehaving { foo() { locked = true; // ... throw new Whatever(); // ... locked = false; }In this case, you have presumably detected a condition that requires you to throw a Whatever exception. It is, therefore, your responsibility to reestablish the invariant prior to throwing such an exception. foo() { locked = true; // ... if (condition == true) { locked = false; throw new Whatever(); } // ... locked = false; } If, on the other hand, you are simply calling another method which itself may throw some exception, then it would seem to be your responsibility to provide an appropriate catch handler in foo() to reestablish the invariant in case that exception is thrown. Alternatively, you might get away with reestablishing the invariant in the finally clause. foo() { locked = true; // ... try { bar(); // May raise an exception } catch (Whatever ex) { locked = false; } // ... locked = false; } Eiffel has the notion of "exception correctness" (Eiffel: The Language, section 15.10, page 258), which is similar to what I described above. Ken Carpenter
Feb 12 2003
"Ken Carpenter" <kencr shaw.ca> writes:Yeah, you can never know what happens - and that's more or less the point of exceptions that you don't really have to worry about them. Of course, you *should* prepare for exceptions to preserve the invariant. The point is, instead of a crash and a non-informative message along the lines of "Invariant of BadlyBehaving violated" or "Exception thrown during unwind" or even "(aborted)", I'd like to get some knowledge about where and when it happened, and in context of what exceptions.foo() { locked = true; // ... throw new Whatever(); // ... locked = false; }In this case, you have presumably detected a condition that requires you to throw a Whatever exception. It is, therefore, your responsibility to reestablish the invariant prior to throwing such an exception. If, on the other hand, you are simply calling another method which itself may throw some exception, then it would seem to be your responsibility to provide an appropriate catch handler in foo() to reestablish the invariant in case that exception is thrown. Alternatively, you might get away with reestablishing the invariant in the finally clause.Eiffel has the notion of "exception correctness" (Eiffel: The Language, section 15.10, page 258), which is similar to what I described above.I suppose OOSC has also something about the subject. (I'm currently in my way through that book) -Antti
Feb 12 2003
Antti Sykari <jsykari gamma.hut.fi> writes:Someone mentioned that exceptions should be allowed to be thrown also when an exception is active, that is, during the unwinding of stack.A more precise formulation of the problem, or an abstract, if you will: Exception handling allows a programming language to concentrate its error-handling code in one place, instead of sprinkling it around in a modularity-violating and bug-prone manner. It works well when the exception patterns are simple and well-behaved. But what if another exception occurs when we are recovering from an exception? One solution is to abort the execution, such as C++ does. Indeed, one basic rule in C++ is: "Don't throw exceptions in destructors." However, this seems like an arbitrary and unnecessarily restrictive rule. There are plenty of reasons why one might want to throw in destructors; for example, class invariants can be modeled as auto classes which throw an InvariantException in their destructor. We can contrast this with interrupt handling in operating systems. When an interrupt occurs, we store the current execution context and proceed to handle the interrupt. After that, we return from the interrupt handler and resumt the execution. But another interrupt can occur whenever we are handling an interrupt. What to do then? One solution is to disable the interrupts while handling the interrupt. Another one is to put the interrupts in queue (usually this is done when the actions required by them are of a lower priority then the current one), and yet another -- if the new interrupt is of higher priority -- is to store the current context and start to handle the new one, and process the current interrupt after the more urgent one. Exceptions are a bit different from interrupts, but my claim here is that one should prepare for the situation where we must process exceptions upon exceptions upon exceptions -- an exceptionally exceptional situation, but possible anyhow. The obvious question I didn't have time to address in the previous post was: * What is the behavior of multiple simultaneous exceptions? * or "How on Earth could it work?" For this I have the following rather straightforward proposal, probably already invented by someone else in some other language: At all times of stack unwinding, we have a list of active exceptions. We proceed to unwind the stack and call the destructors of auto classes. (* -- see comment below) If a new exception is thrown during the process, it is added to the list of active exceptions. Whenever a handler is encountered, it is matched against the active exceptions and either the most specialized exception or the first matching exception in the list is given to the handler. (I'm can't decide my side on this -- opinions?) Handler code is executed and the handled exception is removed from the active exceptions. The unwinding process is continued until there are no active exceptions left, after which the execution is continued after the last matching handler. I suppose it can be proved that with the unwinding method described above, every thrown exception propagates to the nearest exception handler that can handle it, assuming that each exception handler can handle precisely one exception. If multiple active exceptions are allowed, there would be a need for catch-all exception handlers, which could be used effectively to insulate the damage to just one subsystem. Obviously, the top-level catch-all handler should be like this. An example top-level handler: try { // do things } catch_all (Exception e) { print("Caught exception ", e, ":\n"); print(e.stackTrace()); } (*) Checking the invariant of the class can be effectively modeled as an "auto" object which is implicitly placed in each member function; in constructor it acquires a recursive mutex, and if it was the first one to grab it, it checks the invariant. (This would not be needed in a completely type-safe world - but you can never know if the object is modified by a pointer gone haywire or something) In the destructor it releases the mutex, and if it's the last one, it checks the invariant. If the invariant is false, it throws an InvariantException. Suppose we have the previous example, quoted below with line numbers. Also suppose we have nice stack traces as in Java :) The program int main() { f(); } would, then, result in: Got Exception InvariantException() thrown in line 16 (BadlyBehaving::invariant()) at line xxx (stack unwind from exception Whatever, thrown at line 7 (BadlyBehaving::foo)) at line yyy at ... hmm, not quite sure what would come here actually :) but the idea is like this. I can only assume that stack traces come with all kinds of complications which I cannot think of, but the general idea sounds implementable to me. If it is actually needed is a completely different question and probably more of a question of opinion than anything else. And as a disclaimer I can say that I've never implemented an exception handling mechanism so I might be wrong. -Antti The code: 1 > class BadlyBehaving > { > foo() > { 5 > locked = true; > // ... > throw new Whatever(); > // ... > locked = false; 10 > } > > bit locked = false; > > invariant() 15 > { > assert(!locked); > } > } > 20 > f() > { > BadlyBehaving b; > > try { 25 > b.foo(); > } catch (Whatever x) { > // handle whatever > } > 30 > // but now b is in a bad state > }now f() calls b.foo(), which causes an exception to be thrown, and when it exits the scope of BadlyBehaving.foo(), the class invariant is violated. Hence, InvariantException or similar should be thrown, but an exception is already being thrown so we have a problem. If two (or more) exceptions could be active at the same time, this would allow us to catch the invariant violation. Which is nice if you want to have some kind of exception safety guarantees. -Antti
Feb 12 2003
In article <86y94m0y4f.fsf hoastest1-8c.hoasnet.inet.fi>, Antti Sykari says...Someone mentioned that exceptions should be allowed to be thrown also when an exception is active, that is, during the unwinding of stack. As we know, in C++ and probably in D, too, throwing exceptions during stack unwinding results in a catastrophe of some kind, probably abort(). So, what happens when an exception escapes a member function of a class, and leaves that object's invariant in a bad state? class BadlyBehaving { foo() { locked = true; // ... throw new Whatever(); // ... locked = false; } bit locked = false; invariant() { assert(!locked); } } f() { BadlyBehaving b; try { b.foo(); } catch (Whatever x) { // handle whatever } // but now b is in a bad state } now f() calls b.foo(), which causes an exception to be thrown, and when it exits the scope of BadlyBehaving.foo(), the class invariant is violated. Hence, InvariantException or similar should be thrown, but an exception is already being thrown so we have a problem. If two (or more) exceptions could be active at the same time, this would allow us to catch the invariant violation. Which is nice if you want to have some kind of exception safety guarantees. -AnttiHi, In Java (since 1.4) exceptions can have a cause (it's just a field in the Exception class), so you can chain exceptions. This mechanism is used to keep exception information in a wrapper. But something similar could be used in the base exception class: class Exception { Exception cause(); Exception shadowed(); } The compiler can do some "magic" to preserve shadowed exceptions: void doStuff(Thing any) { breakInvariant(); auto Something some = new Something(); // destructor throws DestructorException try { any.doOtherStuff(); // throw AnyException restoreInvariant(); } finally { some.doSomeStuff(); // throws OtherException } } The final exception would be InvariantException, shadowing DestructorException, shadowing OtherException, shadowing AnyException (I think I've got the order right). Note that any of these can also have the cause field with some other exception (e.g. AnyException caused by SQLError and OtherException caused by FileNotFound). IMO the cause field should be filled manually by the programmer (in the constructor call or maybe a setter), but the shadowed field should be automatically filled by the compiler (because keeping track of exceptions shadowing exceptions shadowing... ad infinitum is pretty much boring). Best regards, Daniel Yokomiso. "Philosophy is a battle against the bewitchment of our intelligence by means of language." - Ludwig Wittgenstein
Feb 12 2003