Next: C++/CLI limitations?
From: kanze on 5 Jul 2005 09:12 Niklas Matthies wrote: > On 2005-07-03 10:19, Maxim Yegorushkin wrote: > > On Sat, 02 Jul 2005 14:23:03 +0400, Alf P. Steinbach wrote: > >>> If the assertion fails when there is no debugger, how do > >>> you expect the program to recover? > >> That's actually a good _C++_ question... ;-) > >> First, the reason why one would like to 'throw' in this > >> case, which is usually not to recover in the sense of > >> continuing normal execution, but to recover enough to do > >> useful logging, reporting and graceful exit on an end-user > >> system with no debugger and other programmer's tools > >> (including, no programmer's level understanding of what > >> goes on). > > Why would one want a graceful exit when code is broken, > > rather than dying as loud as possible leaving a core dump > > with all state preserved, rather than unwound? > Because the customer expects and demands it. Actually, more > often than not the customer even demands graceful resumption. I guess it depends on the customer. None of my customers would ever have accepted anything but "aborting" anytime we were unsure of the data. Most of the time, trying to continue the program after an assertion failed would have been qualified as "grobe Fahrlýssigkeit" -- I think the correct translation is "criminal negligence". But I'm not sure that that was the question. My impression wasn't that people were saying, continue, even if you don't know what you are doing. My impression was that we were discussing the best way to shut the program down; basically: with or without stack walkback. Which can be summarized by something along the lines of: trying to clean up, but risk doing something bad, or get out as quickly as possible, with as little risk as possible, and leave the mess. My experience (for the most part, in systems which are more or less critical in some way, and under Unix) is that the operating system will clean up most of the mess anyway, and that any attempts should be carefully targetted, to minimize the risk. Throwing an exception means walking back the stack, which in turn means executing a lot of unnecessary and potentially dangerous destructors. I don't think that the risk is that great, typically, but it is very, very difficult, if not impossible, to really evaluate. For example, I usually have transaction objects on the stack. Calling the destructor without having called commit should normally provoke a roll back. But if I'm unsure of the global invariants of the process, it's a risk I'd rather not take; maybe the destructor will misinterpret some data, and cause a commit, although the transaction didn't finish correctly. Where as if I abort, the connection to the data base is broken (by the OS), and the data base automatically does its roll back in this case. Why take the risk (admittedly very small), when a solution with zero risk exists? But this is based on my personal experience. I can imagine that in the case of a light weight graphical client, for example, the analysis might be different. About all that can go wrong is that the display is all messed up, and in this case, the user will kill the process and restart it manually. And of course, you might luck out, the user might not even notice, and you can pull one over on him. Still, if what the program is doing is important, and not just cosmetic, you must take into account that if the program invariants don't hold, it may do something wrong. If doing something wrong can have bad consequences (and not just cosmetic effects), then you really should limit what you try to do to a minimum. Regardless of what some naive user might think. In such cases, walking back the stack calling destructors represents a significantly greater risk than explicitly executing a very limited set of targetted clean-up operations. -- James Kanze GABI Software Conseils en informatique orientýe objet/ Beratung in objektorientierter Datenverarbeitung 9 place Sýmard, 78210 St.-Cyr-l'ýcole, France, +33 (0)1 30 23 00 34 [ See http://www.gotw.ca/resources/clcm.htm for info about ] [ comp.lang.c++.moderated. First time posters: Do this! ]
From: David Abrahams on 5 Jul 2005 13:53 alfps(a)start.no (Alf P. Steinbach) writes: > * Peter Dimov: >> > > >> > > No recovery is possible after a failed assert. >> >> [The above] means that performing stack unwinding after a failed >> assert is usually a bad idea. > > I didn't think of that interpretation, but OK. > > The interpretation, or rather, what you _meant_ to say in the first > place, AFAICT that was a *conclusion* based on what Peter had said before. > is an opinion, which makes it more difficult to discuss. > After a failed assert it's known that something, which could be anything > (e.g. full corruption of memory), is wrong. Attempting to execute even > one teeny tiny little instruction might do unimaginable damage. Yet you > think it's all right to not only terminate the process but also to log > things, which involves file handling, as long as one doesn't do a stack > rewind up from the point of the failed assert. There are two problems with stack unwinding at that point: 1. It executes more potentially damaging instructions than necessary, since none of the destructors or catch blocks involved have anything to do with process termination or logging. 2. The flow of execution proceeds into logic that is usually involved with the resumption of normal execution, and it's easy to end up back in code that executes as though everything is still fine. > This leads me to suspect that you're confusing a failed assert with > a corrupted stack, or that you think that a failure to clean up 100% > might be somehow devastating. Anyway, an explanation of your > opinion would be great, and this time, please write what you mean, > not something entirely different. Ouch. -- Dave Abrahams Boost Consulting www.boost-consulting.com [ See http://www.gotw.ca/resources/clcm.htm for info about ] [ comp.lang.c++.moderated. First time posters: Do this! ]
From: Peter Dimov on 5 Jul 2005 13:49 David Abrahams wrote: > "Peter Dimov" <pdimov(a)gmail.com> writes: > > > Either > > > > (a) you go the "correct program" way and use assertions to verify that your > > expectations match the observed behavior of the program, or > > > > (b) you go the "resilient program" way and use exceptions in an attempt to > > recover from certain situations that may be caused by bugs. > > > > (a) implies that whenever an assert fails, the program no longer behaves as > > expected, so everything you do from this point on is based on _hope_ that > > things aren't as bad. > > > > (b) implies that whenever stack unwinding might occur, you must assume that > > the conditions that you would've ordinarily tested with an assert do not > > hold. > > And while it is possible to do (b) in a principled way, it's much more > difficult than (a), because once you unwind and return to "normal" > code with the usual assumptions about program integrity broken, you > have to either: > > 1. Test every bit of data obsessively to make sure it's still > reasonable, or > > 2. Come up with a principled way to decide which kinds of > brokenness you're going to look for and try to circumvent, and > which invariants you're going to assume still hold. > > In practice, I think doing a complete job of (1) is really impossible, > so you effectively have to do (2). It's possible to do (b) when you know that the stack unwinding will completely destroy the potentially corrupted state, and it seems possible - in theory - to write programs this way. That is, instead of: void menu_item_4() { frobnicate( document ); } one might write void menu_item_4() { Document tmp( document ); try { frobnicate( tmp ); document.swap( tmp ); } catch( exception const & x ) { // maybe attempt autosave( document ) here report_error( x ); } } Even if there are bugs in frobnicate, if it doesn't leave tmp in an undestroyable state, it's possible to continue. I still prefer (a), of course. :-) [ See http://www.gotw.ca/resources/clcm.htm for info about ] [ comp.lang.c++.moderated. First time posters: Do this! ]
From: David Abrahams on 6 Jul 2005 05:30 alfps(a)start.no (Alf P. Steinbach) writes: >> And while it is possible to do (b) in a principled way, it's much more >> difficult than (a), because once you unwind and return to "normal" >> code with the usual assumptions about program integrity broken, you >> have to either: >> >> 1. Test every bit of data obsessively to make sure it's still >> reasonable, or >> >> 2. Come up with a principled way to decide which kinds of >> brokenness you're going to look for and try to circumvent, and >> which invariants you're going to assume still hold. >> >> In practice, I think doing a complete job of (1) is really impossible, >> so you effectively have to do (2). > > [Here responding to David Abraham's statement:] > > I think your points (1) and (2) summarizes approach (b) well, and show > that it's not a technique one would choose if there was a choice. I think that's what Peter meant when he wrote "performing stack unwinding after a failed assert is usually a bad idea." ^^^^^^^ > But as mentioned above, it's an extreme ^^^^ What is an extreme? > although in some other languages (e.g. Java) you get null-pointer > exceptions & the like. IMO null-pointer exceptions are a joke; it's a way of claiming that the language is typesafe and making that sound bulletproof: all you do is turn programming errors into exceptions with well-defined behavior! Fantastic! Now my program goes on doing... something... even though its state might be completely garbled. >> Note also that once you unwind to "normal" code, information about >> the particular integrity check that failed tends to get lost: all >> the different throw points unwind into the same instruction stream, >> so there really is a vast jungle of potential problems to consider. > > I agree, for the C++ exceptions we do have. > > If we did have some kind of "hard" exception supported by the > language, or even just standardized and supported by convention, > then the vast jungle of potential problems that stands in the way of > further normal execution wouldn't matter so much: catching that hard > exception at some uppermost control level you know that the process > has to terminate, not continue with normal execution (which was the > problem), and you know what actions you've designated for that case > (also known by throwers, or at least known to be irrelevant to > them), so that's what the code has to attempt to do. And what happens if the corrupted programming state causes a crash during unwinding (e.g. from a destructor)? What makes executing the unwinding actions the right thing to do? How do you know which unwinding actions will get executed at the point where you detect that your program state is broken? >> [snip] >> ....and make it someone else's problem. Code higher up the call stack >> mike know how to deal with it, right? ;-) > > In the case of a "hard" exception it's not "might", it's a certainty. ?? I don't care how you flavor the exception; the appropriate recovery action cannot always be known by the outermost layer of the program. Furthermore, what the outermost layer of the program knows how to do becomes irrelevant if there is a crash during unwinding. -- Dave Abrahams Boost Consulting www.boost-consulting.com [ See http://www.gotw.ca/resources/clcm.htm for info about ] [ comp.lang.c++.moderated. First time posters: Do this! ]
From: David Abrahams on 6 Jul 2005 05:59
"Peter Dimov" <pdimov(a)gmail.com> writes: > David Abrahams wrote: >> "Peter Dimov" <pdimov(a)gmail.com> writes: >> >> > Either >> > >> > (a) you go the "correct program" way and use assertions to verify that your >> > expectations match the observed behavior of the program, or >> > >> > (b) you go the "resilient program" way and use exceptions in an attempt to >> > recover from certain situations that may be caused by bugs. >> > >> > (a) implies that whenever an assert fails, the program no longer behaves as >> > expected, so everything you do from this point on is based on _hope_ that >> > things aren't as bad. >> > >> > (b) implies that whenever stack unwinding might occur, you must assume that >> > the conditions that you would've ordinarily tested with an assert do not >> > hold. >> >> And while it is possible to do (b) in a principled way, it's much more >> difficult than (a), because once you unwind and return to "normal" >> code with the usual assumptions about program integrity broken, you >> have to either: >> >> 1. Test every bit of data obsessively to make sure it's still >> reasonable, or >> >> 2. Come up with a principled way to decide which kinds of >> brokenness you're going to look for and try to circumvent, and >> which invariants you're going to assume still hold. >> >> In practice, I think doing a complete job of (1) is really impossible, >> so you effectively have to do (2). > > It's possible to do (b) when you know that the stack unwinding will > completely destroy the potentially corrupted state, and it seems > possible - in theory - to write programs this way. <snip example that copies program state, modifies, and swaps> What you've just done -- implicitly -- is to decide which kinds of brokenness you're going to look for and try to circumvent, and which invariants you're going to assume still hold. For example, your strategy assumes that whatever broke invariants in the copy of your document didn't also stomp on the memory in the original document. Part of what your strategy does is to increase the likelihood that your assumptions will be correct, but if you're going to go down the (b)(2) road in a principled way, you have to recognize where the limits of your program's resilience are. -- Dave Abrahams Boost Consulting www.boost-consulting.com [ See http://www.gotw.ca/resources/clcm.htm for info about ] [ comp.lang.c++.moderated. First time posters: Do this! ] |