boostorg / leaf

Lightweight Error Augmentation Framework
Boost Software License 1.0
302 stars 48 forks source link

Better documentation of the core mechanism #30

Closed Xeverous closed 2 years ago

Xeverous commented 2 years ago

It is possible to get more information how leaf is implemented? I'm particulary interested in the on_error and similar mechanisms which add information about the error context without affecting error-neutral and error-producing function implementations. Somehow leaf knows whether that extra information is useful based on the handler, but the handler can be defined in a separate translation unit.

I would like more information about the internal mechanism:

zajo commented 2 years ago

Right, this isn't explicitly explained. The way it works is, when you give LEAF a list of error handlers, it parses their argument types, and (internally) instantiates the leaf::context class template with these types. For example if you do:

leaf::try_catch(
    []
    {
        ....
    },

    []( my_error_type x )
    {
        ....
    },

    []( your_error_type x, int y )
    {
        ....
    } );

then LEAF will create an object of type context<my_error_type, your_error_type, int>, local to the leaf::try_catch function. This object contains a std::tuple of the requested types. As explained in https://boostorg.github.io/leaf/#context, when the context is activated, thread-local pointers of the relevant types (in this case my_error_type, your_error_type, int) are pointed to the internal std::tuple.

How do leaf::on_error and leaf::new_error "know" if a type is needed or not? If the corresponding thread-local pointer is null, they know that nobody cares about that type, because no active context in the calling thread uses it, because no error handler takes it as an argument.

The library does not use compiler-specific features. If RTTI is enabled, LEAF uses dynamic_cast in the exception handling API. Do note that try_catch catches all exceptions, and will match error handlers even if they don't handle exception types.

Xeverous commented 2 years ago

If the corresponding thread-local pointer is null

How that pointer is determined to be null? How is the pointer even visible? If it is not a void pointer there must be some mechanism to create one, but how a error-neutral function implementation know that such variable exists? Is the solution based on visibility of symbols? Can you make an MCVE (without including leaf)?

zajo commented 2 years ago

Using the terminology from the LEAF implementation, a context<E...> contains std::tuple<leaf_detail::slot<E>...>. Therefore the thread-local pointers are of type leaf_detail::slot<T>, defined as:

template <class E>
struct tl_slot_ptr
{
    static thread_local slot<E> * p;
};

template <class E>
thread_local slot<E> * tl_slot_ptr<E>::p;

The on_error machinery simply uses:

slot<E> * e = tl_slot_ptr<E>::p;

Note that objects with thread_local duration are zero-initialized, so unless you activate a context that contains a type E, the corresponding tl_slot_ptr<E>::p will be null, which would mean that the calling thread does not currently use E to handle errors, and therefore LEAF is free to drop an E object if it is ever passed to it.

This enables libraries to pass to LEAF any and all error information that is relevant to a failure, yet user code is in control of what error types are actually needed; LEAF does not waste cycles transporting error objects that are not useful at the time.

Xeverous commented 2 years ago

Nice. So it's not a symbol-based solution but an initialization-based one. The thread local pointer exist in all cases, it just isn't set up.

LEAF does not waste cycles transporting error objects that are not useful at the time.

What's the overhead of such transport vs the overhead of accessing a global thread-local object?

zajo commented 2 years ago

This discusses overhead and contains a few godbolt shares if you're interested in details: https://github.com/boostorg/leaf/blob/develop/benchmark/benchmark.md#show-me-the-code

Xeverous commented 2 years ago

Thanks for the info, I can only add that IMO the thread-local-pointer-initialization mechanism should be mentioned in the docs. I would consider myself very knowledgeable on C++ but reading leaf docs didn't give me sufficiently satisfying explanation on how it's done and it wasn't a good feeling. On the other hand lots of non-savvy boost users understand it as "template magic" and don't bother looking how things are made. Maybe it's just my personal feeling that I like to know what I'm using and so far most libraries were more than happy to explain their primary inventions.

zajo commented 2 years ago

Are you saying that the information in https://boostorg.github.io/leaf/#context is insufficient, or is it that you think that these implementation details need to be presented more prominently? Do you have any other input on the documentation, especially the introduction?

Xeverous commented 2 years ago

Just more on how it works from inside. I get the API. I easily get the diagrams about (not) transporting the error object through return values up the stack but the thing I did not get was how the error-neutral functions somehow know that a certain global pointer is available. I did not know what exactly allows error-neutral/producing functions to communicate with the context without knowing where it exists.

zajo commented 2 years ago

Right, the context documentation I linked talks about that, I guess you're saying it isn't prominent enough. Probably needs to go in the tutorial, in the section on the error communication model.