nagle@well.UUCP (John Nagle) (08/22/89)
In C++, unlike C, a considerable fraction of the execution can take place implicitly, within constructors and destructors, rather than in explicitly called functions. But there is no way for a constructor or a destructor to signal an error condition to the invoker. Declarations aren't allowed to fail. But constructors often do such things as open files, windows, devices, or databases. Such operations can fail. What then? At present, we have a few options: 1. Panic and abort the program. This is the Pascal solution, and is one of the reasons nobody does serious work in standard Pascal. 2. Force the caller to do the opening operation separately, then pass a handle for the open object to the class, rather than letting the class handle the open itself. This is the solution chosen for the streams package. One of the major advantages of class declarations, the automatic execution of the destructor upon scope exit, is lost with this approach. 3. If the open fails, put the class instance into a "bad" state, so that later operations on it produce some error condition. This works, but provides no guarantee that the proper check will be made later. Even if it is, special effort must be made to save the information that describes what went wrong during construction and then to retrieve and analyze it later. Well, that's where we are today. What should be done? John Nagle
jordan@Morgan.COM (Jordan Hayes) (08/24/89)
John Nagle <nagle@well.UUCP> asks: What then? Good question! I think your answer (3) is closest to what I do: 3. If the open fails, put the class instance into a "bad" state, so that later operations on it produce some error condition. This requires that all member functions call IsValid() or something before using this and continuing, storing the last error message, etc. and can make your code look ugly. For example: #define DEV "/dev/dialer" #define GOOD_TIME "1 800 424 9090" Frob *p; p = new Frob(DEV); if (p->status < 0) { Error("Frob(%s): %s\n", DEV, p->errStr); delete p; return; } p->Dial(GOOD_TIME); ... I asked a question a while back and got ZERO answers; it was along the lines of "how do you get a constructor to return NULL?" so that the above could be replaced with: if ((p = new Frob(DEV)) == NULL) { /* print error message */ return; } p->Dial(GOOD_TIME); Key to this: there must be no construction (note the explicit "delete" in the 1st example) if it "fails" ... Before C++, I used to write code that sorta did this, i.e., in the following code, the "new Frob" gets replaced with "MakeFrob": Frob * MakeFrob(d) char *d; { int fd; Frob *p; if ((fd = open(d, O_RDWR)) < 0) return((Frob *)NULL); p = malloc(sizeof(Frob)); p->fd = fd; return(p); } I miss it. /jordan
cowan@marob.masa.com (John Cowan) (08/25/89)
In article <13262@well.UUCP> nagle@well.UUCP (John Nagle) writes: > > In C++, unlike C, a considerable fraction of the execution can >take place implicitly, within constructors and destructors, rather >than in explicitly called functions. But there is no way for a >constructor or a destructor to signal an error condition to the ^^^^^^^^^^^^^^^^^^^^^^^^^ >invoker. Declarations aren't allowed to fail. But constructors >often do such things as open files, windows, devices, or databases. >Such operations can fail. What then? This is part of the general C++ weakness in the area of signalling errors. I understand some smart cookies at Bell Labs are thinking about this, but so far without success, due to the very weak and non-portable handling of non-local jumps in the underlying C. What's really needed is something close to the Mesa signalling construct. This is, IMHO, the slickest signal handling around. Mesa allows you to specify something like: catch {block1} using {block2} where block1 is executed, and if a signal(code); statement appears within block1, then block2 gets control. (This is not actual Mesa syntax.) Block2 is typically a switch statement which figures out which signal has been sent. If a signal is not handled by block2, we look further up the stack for higher catchers. Important point: at this time the stack has not been unwound, and it is still possible to return to block1! The options of each case within block2 are: reject the signal, causing a search for higher catchers; return from the signal, which returns a value to the caller of signal() as if nothing special had happened, just a function call; abort, which causes the successor of the catch construct to be executed -- everything from block1 is wiped off the stack; retry, which causes everything from block1 to be wiped off the stack and then block1 is restarted from the beginning. -- Internet/Smail: cowan@marob.masa.com Dumb: uunet!hombre!marob!cowan Fidonet: JOHN COWAN of 1:107/711 Magpie: JOHN COWAN, (212) 420-0527 Charles li reis, nostre emperesdre magnes Set anz toz pleins at estet in Espagne.
nusbaum@meson.uucp (R. James Nusbaum) (08/25/89)
In article <13262@well.UUCP> nagle@well.UUCP (John Nagle) writes: > > In C++, unlike C, a considerable fraction of the execution can >take place implicitly, within constructors and destructors, rather >than in explicitly called functions. But there is no way for a >constructor or a destructor to signal an error condition to the >invoker. Declarations aren't allowed to fail. But constructors >often do such things as open files, windows, devices, or databases. >Such operations can fail. What then? > > > John Nagle One solution I used was to define a signal (SIGUSR1) as the 'constructor failed' signal. Then my constructors would raise this signal whenever they failed while initializing an object on the stack. The user could then define a signal handler for this signal or just let the signal halt the program. Obviously this won't work on systems without signals and I believe the method used by constructors to determine if the object was on the stack was also not very portable. Jim Nusbaum Radiation Research Laboratory Loma Linda University Medical Center nusbaum%proton.uucp@ucrmath.ucr.edu
ttwang@polyslo.CalPoly.EDU (Thomas Wang) (08/25/89)
In article <13262@well.UUCP> nagle@well.UUCP (John Nagle) writes: >In C++, unlike C, a considerable fraction of the execution can >take place implicitly, within constructors and destructors, rather >than in explicitly called functions. But there is no way for a >constructor or a destructor to signal an error condition to the >invoker. You have hit a major problem of C++, that it does not have an exception handling mechanism. The programmer has to handle errors in an ad-hoc manner. error checking in normal functions are tedious enough. And error checking in constructor and destructor are particularly nasty. You can have a variable in a class that tells if its state is in a good state. But this will cause the deferred handling of errors and an increase in complexity. > John Nagle -Thomas Wang ("I am, therefore I am." - Akira ) ttwang@polyslo.calpoly.edu
ttwang@polyslo.CalPoly.EDU (Thomas Wang) (08/25/89)
cowan@marob.masa.com (John Cowan) writes: >This is part of the general C++ weakness in the area of signalling errors. >I understand some smart cookies at Bell Labs are thinking about this, but >so far without success, due to the very weak and non-portable handling of >non-local jumps in the underlying C. I would suggest to do a check on the failure variable after every function call, and if a failure has occurred then goto the local failure handling label. There is always a local failure handling label, even if the code at the label only calls some destructors, then returns to the caller. We cannot directly pop multiple stack frames, because the destructors will not be called. Therefore the only alternative is to pop one stack frame at a time and to dutifully call the destructors. In languages without destructors (e.g. Eiffel), we can directly pop multiple stack frames. Since C++ chooses the path of no garbage collection, there will have to be some overhead for the failure handling mechanism. We then have a choice to make. Do we tell the function about the address of the failure handler, or should we check for failure status after the function call is completed. It seems to me the second choice is better. An important advantage of this scheme is that a C program calling C++ functions can use the failure handling mechanism. The C program can just test the failure variable after the C++ function call. If the failure variable is a register variable, the overhead should not be too great. -Thomas Wang ("I am, therefore I am." - Akira ) ttwang@polyslo.calpoly.edu
nagle@well.UUCP (John Nagle) (08/27/89)
In article <13789@polyslo.CalPoly.EDU> ttwang@polyslo.CalPoly.EDU (Thomas Wang) writes: >cowan@marob.masa.com (John Cowan) writes: > >I would suggest to do a check on the failure variable after >every function call, and if a failure has occurred then goto the local >failure handling label. There is always a local failure handling label, >even if the code at the label only calls some destructors, then returns >to the caller. No good. What if failure is detected within a declaration, and only some of the objects associated with that block have been created at that point? The "local failure handling label" concept can't handle this, lacking information about exactly which constructors have been properly instantiated. An even worse case comes when a constructor of a base class fails while construction of a derived class is underway. Imagine instantiating "menu" and having "window" fail at a lower level because too many windows are open. When thinking about how to do this, it's worth remembering the design criterion for the exception handling mechanism in Ada that the overhead for the case in which no exceptions occur must be minimized, even if this makes exception handling slow. John Nagle
ttwang@polyslo.CalPoly.EDU (Thomas Wang) (08/29/89)
nagle@well.UUCP (John Nagle) writes: >In article <13789@polyslo.CalPoly.EDU> ttwang@polyslo.CalPoly.EDU (Thomas Wang) writes: >>I would suggest to do a check on the failure variable after >>every function call, and if a failure has occurred then goto the local >>failure handling label. There is always a local failure handling label, >>even if the code at the label only calls some destructors, then returns >>to the caller. > No good. What if failure is detected within a declaration, and >only some of the objects associated with that block have been created >at that point? The "local failure handling label" concept can't handle >this, lacking information about exactly which constructors have been properly >instantiated. This is easily handled. If there is only one local failure handler, you can have a local variable that tells the location of the error. a::a() { int location = 0; do_something(); location = 1; do_something2(); location = 2; .... } Beside, if the language can implement one local failure handler, why can't it have multiple local failure handlers? This would solve the problem nicely. > An even worse case comes when a constructor of a base class fails >while construction of a derived class is underway. Imagine instantiating >"menu" and having "window" fail at a lower level because too many >windows are open. This is not a problem either. The failure will be passed upward toward the main() function, until the program is cleanly aborted. The failure from 'window' is passed to 'menu' after 'window' cleans up its structure. The 'menu' then handles the failure, cleans up its structure, and pass the failure upward... > When thinking about how to do this, it's worth remembering the >design criterion for the exception handling mechanism in Ada that >the overhead for the case in which no exceptions occur must be >minimized, even if this makes exception handling slow. The destructor of C++ presents big problems to the implementors of exception handling. When a failure occurred, all the destructors for local variables must be called up to the point where we have a failure handler available. We have two choices: We can let destructors to be handled locally, and check failure after every function call. We can also implement a local variable registry to handle the calling of destructors in a global fashion. I suspect the overhead for checking failure after every function call is actualy smaller. The paper "Exception Handling Without Language Extensions" in the 1988 Usenix C++ Conference describes a local variable registry implementation outside the language itself. So this method is at least known in the computing world. > John Nagle -Thomas Wang (Ah so desu ka!) ttwang@polyslo.calpoly.edu