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.