From mboxrd@z Thu Jan 1 00:00:00 1970 X-Spam-Checker-Version: SpamAssassin 3.4.4 (2020-01-24) on polar.synack.me X-Spam-Level: X-Spam-Status: No, score=-1.9 required=5.0 tests=BAYES_00 autolearn=ham autolearn_force=no version=3.4.4 X-Google-Language: ENGLISH,ASCII-7-bit X-Google-Thread: fac41,b87849933931bc93 X-Google-Attributes: gidfac41,public X-Google-Thread: 1108a1,b87849933931bc93 X-Google-Attributes: gid1108a1,public X-Google-Thread: f43e6,b87849933931bc93 X-Google-Attributes: gidf43e6,public X-Google-Thread: 103376,b87849933931bc93 X-Google-Attributes: gid103376,public X-Google-Thread: 114809,b87849933931bc93 X-Google-Attributes: gid114809,public X-Google-Thread: 109fba,b87849933931bc93 X-Google-Attributes: gid109fba,public From: bobduff@world.std.com (Robert A Duff) Subject: Re: Exceptions as objects (was Re: What is wrong with OO ?) Date: 1997/01/25 Message-ID: X-Deja-AN: 212192703 references: <5acjtn$5uj@news3.digex.net> organization: The World Public Access UNIX, Brookline, MA newsgroups: comp.lang.c++,comp.lang.smalltalk,comp.lang.eiffel,comp.lang.ada,comp.object,comp.software-eng Date: 1997-01-25T00:00:00+00:00 List-Id: In article , Piercarlo Grandi wrote: >The obvious and correct solution is to make the name of the procedure >dynamically scoped; then it can be redefined whenever this is needed. Yes, it is well known that exception handling can be modeled as dynamically-scoped procedure names. And you explained that quite clearly. What is not clear (to me) is why you think that way of doing things is so much better. >Thus: > >* an exception is a fact, not an object; > >* the fact is the absence of a suitable 'else' somewhere when it should > be there; Why "should be"? What's wrong with partial functions? I mean, it seems perfectly reasonable to define "square root" such that negative numbers don't have square roots. Other definitions are possible (e.g. complex numbers), but this definition is certainly a reasonable one. >Finally, recovery from an exception may involve non local control >transfers out of the procedure that handles the exception; but this is >an entirely separate issue. It need not be the case. It is almost always the case. >So, basically *everything* is wrong (and clumsily so) with the ``C++'' >exception-as-object system: > >* the name of the exception class should be that of a dynamically scoped > procedure. > >* an exception object is really just a clumsy and silly way of > specifying an argument list to that dynamically scoped procedure. You use words like "clumsy" and "silly", but you don't back them up with real arguments very well. >* non local control transfers are then mandatory. This seems OK to me (see below). >Consider the stark contrast between (in some language reminiscent of >``C++''): [example snipped] >and [C++ example snipped] >As you can see the second solution is really just a clumsy, misleading, >limited version of the first. Sorry, but no, I can't see that. They look more-or-less the same to me. Here are some pieces of your example: > try { printf("%f\n",sqrt(-2.0)); } > catch(sqrt_neg_arg &sna) > { > // cannot just return 0.0 -- too bad. > puts("0.0"); // not really what we wanted > } Why "not really what we wanted"? If the goal is to "print out the square root of the number, but if its negative, print 0.0", then the above code seems to do that pretty clearly. In your "fluid" example, on the other hand, you trick the sqrt function into returning a bogus value for a negative argument -- this doesn't seem clearer to me. Alternatively, if you really want a sqrt function that returns zero for negative arguments, why not write a wrapper function, which calls sqrt, and has an exception handler saying "return 0.0"? This is what you need to do anyway, in the general case -- suppose I want to have square root of a negative number return a complex number? Well, I need a wrapper function, since this new functionality has a different result type from your sqrt, which returns a float. But in any case, you normally don't want to print "0.0" for the square root of -2 -- you want to print an error message, or do something else entirely different, as in the next case: > try { printf("%f\n",sqrt(-2.0)); } > catch(sqrt_neg_arg &sna) > { > fprintf(stderr,"panic: negative arg to 'sqrt': %f\n",sna.f); > abort(); > } I claim that this is the sort of thing you usually want to do when you handle an exception -- do something entirely different, and not just pretend that all is well. And your "fluid" example doesn't seem to do this any better. (Of course *most* exceptions aren't handled at all -- they're simply bugs in the program, and you want to get rid of bugs before your customers see them. For this case, the value of an exception is just in pinpointing the error, for ease of debugging.) >Note that in effect in ``C++'' the 'fluid' storage class that turns a >variable into a dynamically scoped one is pretty easy to simulate (in >the shallow binding way) using constructors/destructors: [example snipped] >which seems rather legible, vastly more flexible/reasonable than the >convoluted and limiting current ``C++'' syntax. Again, why do you think this is better than using C++ exception handling? I'm reading this on comp.lang.ada, and Ada is a multi-threaded language. Your simulation of dynamic scoping doesn't quite work in a multi-threaded environment. You need to make the function pointer into per-task data. Note that this simulated dynamic scoping is likely to be much less efficient than a (good) implementation of exception handling. Exception handling can (and should) be implemented using a PC-table-lookup strategy, which makes it near-zero overhead to enter an exception- handled region. >Now there is completely orthogonal problem of non local control >transfers, which might well be useful in a procedure implementation that >``handles'' an exception. In the above code snippets an example of the >use of non local control transfers is the call to 'abort()', which exits >the current process and transfers control back to the invoking process; >one could use 'setjmp()' or 'longjmp()', or something better, for finer >grained control transfers. A goto statement is what you want, but you want to goto an outer procedure -- this is allowed in Pascal, for example. And you want nested procedures, of course. (Of course, you can't use the word "goto" in the name of a feature, or people will think you're evil. On the other hand, you can have a feature with semantics similar to goto, and people will be perfectly happy -- so long as you don't call it goto.) >It is a pity that the ``C++'' exception handling features tie so >intimately together the distinct concepts of dynamically scoped >identifiers (and then they are provided in a rather clumsy form) and of >non local control transfers (and then they are also provided in a rather >clumsy form). OK, I guess I can see part of what you're saying: dynamic scoping and non-local gotos ought to be two separate language features, each usable in its own right, rather than combining them into a single feature (exception handling). (The "clumsy" doesn't add much to your argument, since you don't really say what's so clumsy.) Is this it? I guess there's some merit in that -- I like orthogonality. Other than this, I can't see any advantage of your scheme. One advantage of the exceptions-as-objects viewpoint is that you can have a hierarchy of exception classes. This allows the call site to determine the granularity of the exceptions to be handled. E.g. handle any I/O exception, or handle just the end-of-file exception, or handle *all* exceptions. This seems like a valuable capability, and I don't see any way to do it in your "fluid" language. E.g., suppose I want to say, in the main program, "If I get *any* exception, send a signal to the so-and-so device, telling it to shut down, and then I want to kill the program"? Another issue is efficiency: I don't see any way to implement your "fluid" feature with near-zero overhead on entering and leaving the scope of a fluid object. Exception handlers can be implemented efficiently, because upon entering the scope of the handler, you can know at compile time the address of the handler. With fluid variables, how do I know (at the original declaration point) that somebody won't make a binding to some dynamic value? Another issue is stack overflow: In Ada, if the stack overflows, you get a Storage_Error exception. The exception handler is executing *after* chopping back the stack (i.e. after doing the non-local goto), so the handler can reasonably do some work. In your scheme, the handler is a function, which is called while the stack is still "full". You need some way of ensuring that this function has enough stack space to do its job -- perhaps hold a little bit of stack space "in reserve", so the function can at least do a non-local goto. This is easier in the Ada case, since the only such function is the run-time system's Raise_Exception procedure, which the compiler can know about. I realize this is irrelevant to C++, where you can't even *try* to handle stack overflows. > As to this, I rememjber reading someone stating that as far as he knew > only TECO (yes, TECO!) of most languages around provides general clean > entry/exit scope functions/triggers. Ah well. What are "entry/exit scope functions/triggers". (I don't know much about TECO, except that it is terse to the point of looking like line noise.) - Bob