DConf talk : Exceptions will disappear in the future?

H. S. Teoh hsteoh at quickfur.ath.cx
Tue Jan 5 21:46:46 UTC 2021


On Tue, Jan 05, 2021 at 06:23:25PM +0000, sighoya via Digitalmars-d-learn wrote:
> Personally, I don't appreciate error handling models much which
> pollute the return type of each function simply because of the
> conclusion that every function you define have to handle errors as
> errors can happen everywhere even in pure functions.

Yesterday, I read Herb Sutter's proposal on zero-overhead deterministic
exceptions (for C++):

	http://open-std.org/JTC1/SC22/WG21/docs/papers/2018/p0709r0.pdf

tl;dr:

1) The ABI is expanded so that every (throwing) function's return type
is a tagged union of the user-declared return type and a universal error
type.

    a) The tag is implementation-defined, and can be as simple as a CPU
    flag or register.

    b) The universal error type is a value type that fits in 1 or 2 CPU
    registers (Herb defines it as the size of two pointers), so it can
    be returned in the usual register(s) used for return values.

2) The `throw` keyword becomes syntactic sugar for returning an instance
of the universal error type. The `return` keyword becomes syntactic
sugar for returning an instance of the declared return value (as before
-- so the only difference is clearing the tag of the returned union).

3) Upon returning from a function call, if the tag indicates an error:

    a) If there's a catch block, it receives the returned instance of
    the universal error type and acts on it.

    b) Otherwise, it returns the received instance of the universal
    error type -- via the usual function return value mechanism, so no
    libunwind or any of that complex machinery.

4) The universal error type contains two fields: a type field and a
context field.

    a) The type field is an ID unique to every thrown exception --
    uniqueness can be guaranteed by making this a pointer to some static
    global object that the compiler implicitly inserts per throw
    statement, so it will be unique even across shared libraries. The
    catch block can use this field to determine what the error was, or
    it can just call some standard function to turn this into a string
    message, print it and abort.

    b) The context field contains exception-specific data that gives
    more information about the nature of the specific instance of the
    error that occurred, e.g., an integer value, or a pointer to a
    string description or block of additional information about the
    error (set by the thrower), or even a pointer to a
    dynamically-allocated exception object if the user wishes to use
    traditional polymorphic exceptions.

    c) The universal error type is constrained to have trivial move
    semantics, i.e., propagating it up the call stack is as simple as
    blitting the bytes over. (Any object(s) it points to need not be
    thus constrained, though.)

The value semantics of the universal error type ensures that there is no
overhead in propagating it up the call stack.  The universality of the
universal error type allows it to represent errors of any kind without
needing runtime polymorphism, thus eliminating the overhead the current
exception implementation incurs.  The context field, however, still
allows runtime polymorphism to be supported, should the user wish to.

The addition of the universal error type to return value is automated by
the compiler, and the user need not worry about it.  The usual try/catch
syntax can be built on top of it.

Of course, this was proposed for C++, so a D implementation will
probably be somewhat different.  But the underlying thrust is:
exceptions become value types by default, thus eliminating most of the
overhead associated with the current exception implementation. (Throwing
dynamically-allocated objects can of course still be supported for users
who still wish to do that.)  Stack unwinding is replaced by normal
function return mechanisms, which is much more optimizer-friendly.

This also lets us support exceptions in @nogc code.


[...]
> The other point is the direction which is chosen in Go and Rust to
> make error handling as deterministic as possible by enumerating all
> possible error types.
> Afaict, this isn't a good idea as this increases the fragile code
> problem by over specifying behavior. Any change requires a cascade of
> updates if this is possible at all.

There is no need for a cascade of updates if you do it right. As I
hinted at above, this enumeration does not have to be a literal
enumeration from 0 to N; the only thing required is that it is unique
*within the context of a running program*.  A pointer to a static global
suffices to serve such a role: it is guaranteed to be unique in the
program's address space, and it fits in a size_t.  The actual value may
differ across different executions, but that's not a problem: any
references to the ID from user code is resolved by the runtime dynamic
linker -- as it already does for pointers to global objects.  This also
takes care of any shared libraries or dynamically loaded .so's or DLLs.


[...]
> No error handling model was the HIT and will never be, therefore I
> would recommend to leave things as they are and to develop
> alternatives and not to replace existing ones.

I've said this before, that the complaints about the current exception
handling mechanism is really an issue of how it's implemented, rather
than the concept of exceptions itself.  If we implement Sutter's
proposal, or something similar suitably adapted to D, it would eliminate
the runtime overhead, solve the @nogc exceptions issue, and still
support traditional polymorphic exception objects that some people still
want.


T

-- 
Philosophy: how to make a career out of daydreaming.


More information about the Digitalmars-d-learn mailing list