Boost logo

Boost :

Subject: Re: [boost] Error handling libraries and proposals
From: Emil Dotchevski (emildotchevski_at_[hidden])
Date: 2017-07-11 00:32:33


On Mon, Jul 10, 2017 at 4:19 PM, Niall Douglas via Boost <
boost_at_[hidden]> wrote:

> On 10/07/2017 22:06, Emil Dotchevski via Boost wrote:
> > On Mon, Jul 10, 2017 at 1:36 PM, Niall Douglas via Boost <
> > boost_at_[hidden]> wrote:
> >
> >> outcome<void, void> would equal outcome<void, void, std::exception_ptr>.
> >>
> >> Such an object can store a void or an exception_ptr. Or rather, have the
> >> state of a void or an exception_ptr.
> >>
> >
> > So, if I understand correctly, the second void means exception_ptr? Why
> > isn't it outcome<void,exception_ptr>? What are the semantic differences
> > between e.g. expected<int,exception_ptr> and outcome<int,void>?
>
> Are you confusing result and outcome?
>
> result<T, EC>
>
> outcome<T, EC, E|P>
>
> The default template parameters give exactly the same as with v1. But if
> say you chose `result<int, long>` or `outcome<int, long, std::string>`
> then that works too.
>

I still don't understand what does it mean to use void. I don't understand
what this means:

"outcome<void, void> would equal outcome<void, void, std::exception_ptr>"

> >>> , but for Outcome v2 the new values are:
> >>>
> >>> yes, yes, yes, yes, no, yes, yes, yes.
> >>>
> >>>
> >>> To me, your note that Outcome is struct-like rather than variant-like
> >>> means that it does not have strict value-or-error semantics, so the
> last
> >>> one would be "no".
> >>
> >> Unless you enable the positive status support manually, a strict
> >> enforcement of value or error is made.
> >
> >
> > Ah, so this means that it is optional, like in Noexcept. I'll reflect
> that
> > in the comparison table.
>
> Eh, well, it defaults to strict enforcement of either, it can only be
> different if you change a macro. And as the code matures I may make the
> strict enforcement mandatory.
>

I think that this is a key design decision. So is whether or not you have
to enumerate the possible error types.

Take this for what it's worth, but the earlier outcome design was a lot
more focused. Reading the documentation, some of it sounded like
evangelization of std::error_code, so the conclusion I drew was that the
idea is that you don't specify the error types because you should only be
using std::error_code, except that in the real world you might get an
exception from somewhere, in which case outcome lets you stuff a
std::exception_ptr into it too. That makes sense, even if I think that it
is not practical to assume that everyone will jump on the std::error_code
train.

On the other hand, expected<T,E> (Vicente) says no, std::error_code is not
the only option so this should be a template parameter. On top of that,
expected<T,E...> (Peter) says well, at the very least you might get an
exception from somewhere so you have to be able to have e.g.
expected<T,std::error_code,std::exception_ptr>, so we'll take more than a
single E.

The other significant difference you introduced post-review was that
outcome no longer had strict value-or-error semantics. This too helped set
it apart.

But now, and it may be just me, but I am confused. What exactly is the
difference between outcome and the two flavors of expected we have? Is it
essentially the same as expected<T,E...>, except that in outcome the strict
value-or-error semantics (that you may remove later) are optional?

> >>> Also, could you expand on how Outcome can propagate
> >>> errors from a C-style callback or across language boundaries? I'm
> >>> referring to use cases like these:
> >>> https://zajo.github.io/boost-noexcept/#example_lua
> >>
> >> I specifically personally need result to be C layout compatible so AFIO
> >> can have C SWIG bindings. Outcome v1 was C layout compatible too, but v2
> >> is even more so because it's just a struct of the form:
> >>
> >> struct
> >> {
> >> T value;
> >> unsigned flags;
> >> EC error;
> >> };
> >>
> >> ... and bit 0 of the unsigned means a T is present, and bit 2 means an
> >> EC is present.
> >>
> >
> > It seems that here you are making the case I've been making, that in
> > general std::error_code is not sufficient, that an error-handling library
> > must be able to propagate errors of user-defined types, and not only
> > through an exception_ptr.
>
> I did take into account your feedback during the review, yes, and I've
> definitely thrown you some meat in v2.
>

Ha, I'm honored. :)

> > I also think it is a good idea to be
> > C-compatible, but from the above it isn't clear to me how that works.
> Could
> > you please post a complete example, specifically how can outcome<T>
> > propagate errors from a C (not C++) API?
>
> That it cannot do. You could store it into TLS the same as you're doing
> behind the scenes though. Outcome v2 is far more barebones than v1 was.
> It does pretty much nothing above the absolute bare minimum.
>
> > That said, if you look at the example I linked, you'll see that it is
> about
> > more than just being C-layout compatible, but also being able to
> propagate
> > an error from a third-party C-style callback which does not return your
> > struct. In case I'm missing something, could you post the outcome<T>
> > version of the example I linked?
>
> You'd declare some TLS storage, same as you do internally, and reference
> that. So say:
>
> ```
> static thread_local result<void, do_work_error> state;
>
> int do_work( lua_State * L ) noexcept {
> bool success=rand()%2;
> if( success )
> return lua_pushnumber(L,42), 1;
> else
> {
> state = do_work_error{};
> return lua_error(L);
> }
> }
> result<int, do_work_error> call_lua( lua_State * L ) noexcept {
> lua_getfield( L, LUA_GLOBALSINDEX, "call_do_work" );
> if( int err=lua_pcall(L,0,1,0) ) {
> lua_pop(L,1);
> return state;
> }
> else {
> int answer=lua_tonumber(L,-1);
> lua_pop(L,1);
> return answer;
> }
> }
> ```
>
> Which is almost as succinct as yours.
>

This might be reasonable if it is a one-off thing, but consider that the
general case is a bit more complicated, possibly involving different
compilation units, and you do need to ensure that when the thread
terminates "state" doesn't contain an error. It may or may not make sense
to add something to that effect to Outcome.

By the way, this illustrates that errors and successful return values are
very different semantically, which is why at least sometimes it is
necessary to find a different channel to pass errors out. At which point
one must ask what is the downside of using TLS for the error object? What
is the design rationale for insisting on stuffing errors into return
values, moving them one level up at a time, when in reality only the
reporting and the handling code care about the error?

The benefits of always passing errors through TLS are 1) it effectively
decouples successful results from error conditions, which in turn means
that only the error-reporting and the error-handling code are coupled with
the error object or its type; and 2) it doesn't require the enumeration of
all possible error types that a function may "return" (I'll once again
refer the reader to exception specifications as to why that is a bad idea).
The reason why not requiring the enumeration is linked to TLS use is that
it requires the storage to be "large enough", which is perhaps too large to
stuff into a return value (since we can't assume that a dynamic allocation
is permissible.)


Boost list run by bdawes at acm.org, gregod at cs.rpi.edu, cpdaniel at pacbell.net, john at johnmaddock.co.uk