gnu-arch-users
[Top][All Lists]
Advanced

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

Re: [Gnu-arch-users] Nit


From: Robin Farine
Subject: Re: [Gnu-arch-users] Nit
Date: 22 Oct 2003 02:24:14 +0200
User-agent: Gnus/5.09 (Gnus v5.9.0) Emacs/21.2

>>>>> "Tom" == Tom Lord <address@hidden> writes:

    >> The good thing with a library function raising an exception is that
    >> while the call backtracks, intermediate callers that just ignore the
    >> error won't hide it.

    Tom> When you say "callers that just ignore the error" you mean callers
    Tom> that don't put any code to check for the error, right?

Yes.

    Tom> If so, no, that's the _bad_ thing about a library function raising an
    Tom> exception:

I did not say that having an exception mechanism automatically
enforces good error handling design. Just like programming in an OO
language does not magically yield simple and beautiful designs. And it
does not prevent us to create bugs either. It is possible to do bad
thing with any tool, a fortiori, in a very difficult area like error
handling.

    Tom> Consider that I have have a library, B, with a public function
    Tom> specified like this:

    Tom>        parsed_config
    Tom>         B_fn (string user_name)

    Tom>            Find the config file for the indicated user and
    Tom>            return it in parsed form.

    Tom>           May raise exceptions:

    Tom>                no_such_user    The indicated user does not exist.
    Tom>                 ....

People *a lot* more skillful than us in dealing with exception
processing have worked hard on this subject and some patterns of
exception usage emerged. For instance, I seem to remember that mapping
errno style errors to global exception classes for general use is
known to yield bad results.

    Tom> I dutifully write some code that uses the B library:


    Tom>         try {

    Tom>          cfg = B_fn (user);

    Tom>         } except (no_such_user e) {

    Tom>            user = prompt_for_correct_user_name ();
    Tom>            retry;

    Tom>         } ...

You would probably not do that, do you? Maybe something more like:

        while (i++ < NO_MORE_PLEASE) {
          user = prompt_for_user_name();
          try {
            cfg = B_fn(user);
            break;
          } catch (no_such_user e) {
            message("bad user ...");
          }
        }

Anyway, I don't think it is the way to go in this hypothetic problem.

    Tom> This works fine until, later, the implementation of B_fn is modified
    Tom> to also call D_fn.    Perhaps D_fn is a logging function that tries to
    Tom> send a message to the admin user.
    Tom> In making the change to B_fn, the programmer makes a mistake.   He
    Tom> doesn't notice that D_fn can throw a no_such_user exception (if the
    Tom> admin account is missing) and just ignores that case.

Keep in mind that I am no expert at all, but let me try to sketch what
I would do with this example. At a glance, I see two subsystems. One that
deals with configurations and another that provides logging services,
so each deserves its own exception classes.

For instance, the configuration stuff would define a
Configuration::ParseError exception class (where Configuration is a
namespace or a facade class). But B_fn(user) should not raise an
exception when no entry exists for a given user. This case looks more
like a mapping container lookup function for which the absence of
value for a given key is part of the possible results. B_fn(user)
should just return nil or equivalent in this case. However, it could
raise a Configuration::ParseError exception when reading incoherent
data from the persistent image of the configuration.

The logging subsystem could define, among others, a
Logging::DestinationUnreachable exception with a reason field that
says 'user_not_found' or 'connection_refused' or whatever.

Moreover, the omnipresent low level memory management subsystem would
define an OutOfSystemMemory exception (such as std::bad_alloc in C++)
raised when a primitive allocation routine fails.

Now, assuming that the application makes use of a cache allocator to
speed up frequent allocations/deallocations and that nested functions called
from the configuration or logging code allocates some memory, the high
level code could do something like this:

        attempts = 0;
        while (attempts++ < NO_MORE_PLEASE) {
          user = prompt_for_user_name();
          try {
            retries = 0;
            while (retries++ < MAX_RETRIES) {
              try {
                cfg = B_fn(user);
              } catch (OutOfSystemMemory e) {
                allocator_cache.release_some_memory();
              }
            }
            if (cfg)
              break;
            else
              message("bad user ...");
          } catch (Configuration::ParseError e) {
            parse_error(e); /* format error message and output to stderr */
            abort();
          } catch (Logging::DestinationUnreachable e) {
            logging_error(e); /* format error message and output to stderr */
            abort(); /* or maybe ignore it */
          }
        }

This illustrates a case where B_fn() cannot handle an error (out of
memory) because it doesn't know anything about the details of the
underlying memory management system. Thus, it cannot handle this error
for the application and just lets the exception propagate to upper
levels. However, the application knows about the allocator cache (it
has created it) and can try to free some system memory before retrying
the call to B_fn().

This also shows that it is by no means easy to design relevant
exception classes and also that exceptions do *not* always replace a
suitable return value.

    Tom>_ In a language like Java, to get that effect, apparently
    Tom> every call will have to be written (forgive my pseudo-java
    Tom> here):


    Tom>        try {
    Tom>             call ()
    Tom>         } except (error_i_handle e) {
    Tom>           handle
    Tom>         } except (error_i_propogate e) {
    Tom>           throw
    Tom>         } except (any_error e) {
    Tom>           abort
    Tom>         }

This seems to indicate that you identify exceptions with return codes
since doing this is equivalent to passing by reference an error
argument to non pure functions and checking the returned values. As my
above example attempts to show, exceptions provide a tool an order of
magnitude more powerful than return codes. Using them correctly
requires a different way of thinking to design error handling
mechanisms.

    Tom> But there's no getting around it (not for cleanups,
    Tom> necessarily -- just to turn unanticipated exceptions into
    Tom> aborts rather than forwarding them to unsuspecting callers).

C++ offers exactly this by adding exception specifications to function
prototypes. It is not always desirable to to so, though. For instance,
the designer of a template class cannot in general know the exceptions
raised by the types specified as arguments during instantiation of the
template. I think Eiffel or Ada can restrict the exceptions raised by
generic type arguments, but in practice it is very hard to specify
stable APIs using this feature.

    Tom> It'd be nice to have short syntax for:

    Tom> a) abort on _any_ error in this call
    Tom> b) drop _all_ error codes from this call
    Tom> c) forward _all_ error codes from this call

Don't Java exception specifications or C++ (member) function
declarations provide exactly this functionality?

-- 
rnf




reply via email to

[Prev in Thread] Current Thread [Next in Thread]