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: 109fba,b87849933931bc93 X-Google-Attributes: gid109fba,public X-Google-Thread: 103376,b87849933931bc93 X-Google-Attributes: gid103376,public X-Google-Thread: 114809,b87849933931bc93 X-Google-Attributes: gid114809,public X-Google-Thread: 1108a1,b87849933931bc93 X-Google-Attributes: gid1108a1,public X-Google-Thread: f43e6,b87849933931bc93 X-Google-Attributes: gidf43e6,public From: piercarl@sabi.demon.co.uk (Piercarlo Grandi) Subject: Re: Exceptions as objects (was Re: What is wrong with OO ?) Date: 1997/01/25 Message-ID: X-Deja-AN: 212023026 x-nntp-posting-host: sabi.demon.co.uk x-disclaimer: Contents reflect my personal views only references: <5acjtn$5uj@news3.digex.net> <32dd9fc8.262114963@news.sprynet.com> content-type: text/plain; charset=US-ASCII organization: Home's where my rucksack's mime-version: 1.0 (generated by tm-edit 7.94) 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: >>> "doylep" == Patrick Doyle writes: doylep> In article , doylep> Piercarlo Grandi wrote: piercarl> Unfortunately exceptions-as-object is a bad idea, and claiming piercarl> priority for bad ideas is hardly a sport worth engaging piercarl> into. :-) doylep> Could you elaborate on why that is a bad idea? Of course, and easily; somebody asked me the same idea by private e-mail, so I can just paste in the reply(s) and do a little bit of editing. It's also one of my pet peeves... And it is astonishing that what I am going to say, that has been part of the state of the art for some dozen years, is not yet apparently widely known. Let's start with the nature of exceptions. The first point is that if a procedure implements a total function then it will never have an exception. Therefore exceptions arise only and only if a procedure implements a partial function, and arise only and only when the inputs to a procedure are outside its domain. This means that an exception can only arise when at some point in a procedure where there is a 'if' (or similar) statement whose precondition is stronger than the postcondition of the statement that precedes it. For example: float sqrt(float f) { if (f >= 0) return exp(0.5*ln(f)); } Here the precondition of the 'if' is $f >= 0$, but the postcondition preceding it is (more or less) $true$. Now exception handling means extending the precondition of a procedure, up to making it become a total function. This means simply weakening the precondition of 'if's by adding 'else'/'elif' clauses. Consider: extern float sqrt_neg_arg(float); float sqrt(float f) { return (f >= 0) ? exp(0.5*ln(f)) : sqrt_neg_arg(f); } Now 'sqrt' is a total function. This is the general way of``defining'' and ``raising'' exceptions: provide an 'else' that invokes a procedure; the name of the procedure is the name of the ``exception''. Unfortunately in the general case this simply cannot be done as written, because the author of a piece of software may not be in a position to know what to code in those 'else's, for the more appropriate action may depend on the runtime context. It is thus inappropriate to assign a statically defined association between a name like "sqrt_neg_arg" and a procedure implementation. The obvious and correct solution is to make the name of the procedure dynamically scoped; then it can be redefined whenever this is needed. Thus: * an exception is a fact, not an object; * the fact is the absence of a suitable 'else' somewhere when it should be there; * defining and raising an exception means simply adding an 'else' and a call to a dynamically bound procedure; * handling an exception means simply resolving the dyanmically scoped name to a procedure body and executing it. 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. 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. * non local control transfers are then mandatory. Consider the stark contrast between (in some language reminiscent of ``C++''): fluid float sqrt_neg_arg(float); float sqrt(float f) { return (f >= 0) ? exp(0.5*ln(f)) : sqrt_neg_arg(f); } main() { { fluid float sqrt_neg_arg(float f) { return 0.0; } printf("%f\n",sqrt(-2.0)); } { fluid float sqrt_neg_arg(float f) { fprintf(stderr,"panic: negative arg to 'sqrt': %f\n",f); abort(); ); printf("%f\n",sqrt(-2.0)); } } and class sqrt_neg_arg { public: float n; sqrt_neg_arg(float f) : n(f) {} }; float sqrt(float f) { if (f >= 0) return exp(0.5*ln(f)); else throw sqrt_neg_arg(f); } main() { 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 } try { printf("%f\n",sqrt(-2.0)); } catch(sqrt_neg_arg &sna) { fprintf(stderr,"panic: negative arg to 'sqrt': %f\n",sna.f); abort(); } } As you can see the second solution is really just a clumsy, misleading, limited version of the first. Exceptions are not objects; and exception objects are just stupid ways of passing a parameter list to a dynamically scoped procedure name, which is confused with a class. However, in most cases even for exception handling one does not really need dynamically scoped function names; dynamically scoped function pointer variables are about as good, as in (rewriting my example): float sqrt_neg_arg_default(float f) { fprintf(stderr,"sqrt neg arg: %f\n",f); abort(); } fluid float (*sqrt_neg_arg)(float) = sqrt_neg_arg_default; float sqrt(float f) { return (f >= 0) ? exp(0.5*ln(f)) : (*sqrt_neg_arg)(f); } float sqrt_neg_arg_zero(float f) { return 0; } float sqrt_neg_arg_abs(float f) { return sqrt(-f); } main() { { fluid float (*sqrt_neg_arg)(float) = sqrt_neg_arg_abs; printf("%f\n",sqrt(-4.0)); // prints 2.0 } { fluid float (*sqrt_neg_arg)(float) = sqrt_neg_arg_zero; printf("%f\n",sqrt(-42.0)); // prints 0.0 } printf("%f\n",sqrt(-33.0)); // default, aborts } Dynamically scoped function pointers have the advantage that they don't require introducing into the language nested functions, and allow one more easily to separate ``exception handler'' interface and implementation. 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: ------------------------------------------------------------------------ typedef void *Fluid; class FluidFrame { private: Fluid saved; Fluid *variable; public: construct FluidFrame(Fluid *); destruct ~FluidFrame(); }; static inline FluidFrame::FluidFrame ( register Fluid *const avariable) { this->saved = *avariable; /* Save old value */ this->variable = avariable; /* Note where to restore it */ } static inline FluidFrame::~FluidFrame() { *this->variable = saved; } #define fluid(NAME) \ FluidFrame Fluid_##NAME(& (Fluid) (NAME)); NAME #define fluidfor(CLASS,MEMBER,NAME) \ FluidFrame Fluid_##CLASS##_##NAME \ (& (Fluid) (CLASS MEMBER NAME)); \ CLASS MEMBER NAME ------------------------------------------------------------------------ The the example above becomes: float sqrt_neg_arg_default(float f) { fprintf(stderr,"sqrt neg arg: %f\n",f); abort(); } float (*sqrt_neg_arg)(float) = sqrt_neg_arg_default; float sqrt(float f) // implements a total function { return (f >= 0) ? exp(0.5*ln(f)) : (*sqrt_neg_arg)(f); } float sqrt_neg_arg_zero(float f) { return 0; } float sqrt_neg_arg_abs(float f) { return sqrt(-f); } #include main() { { fluid (sqrt_neg_arg) = sqrt_neg_arg_abs; printf("%f\n",sqrt(-4.0)); // prints 2.0 } { fluid (sqrt_neg_arg) = sqrt_neg_arg_zero; printf("%f\n",sqrt(-42.0)); // prints 0.0 } printf("%f\n",sqrt(-33.0)); // default, aborts } which seems rather legible, vastly more flexible/reasonable than the convoluted and limiting current ``C++'' syntax. 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. 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). The issue of non local control transfers, even if only the form of upward funargs, and scope entry/exit triggers, is interesting and ``C++'' provides only ad hoc and limited machinery to deal with it. 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.