Boost logo

Boost :

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

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

> >> 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>"
> The default declaration for outcome is:
> ```
> template <
> class R,
> class S = std::error_code,
> class P = std::exception_ptr,
> class NoValuePolicy = policy::default_outcome_policy<R, S, P>
> >
> requires
> (std::is_void<EC>::value || std::is_default_constructible<EC>::value)
> && (std::is_void<P>::value || std::is_default_constructible<P>::value)
> class [[nodiscard]] outcome;
> ```
> Yay the Concepts TS. Anyway, therefore `outcome<void, void>` equals
> `outcome<void, void, std::exception_ptr>`.
> Does it make sense now?

No. Why would I pass void for S or P?

By the way, such policy-based design is probably not a ideal for
error-handling library return types, for the same reason shared_ptr<T> is
better than Alexandrescu's policy-based smart pointer, and for the same
reason function<T> (as it is now standardized) is better than function<T,A>
(as it was in Boost initially, before I fixed it): it leads to much
increased coupling, and ABI incompatibilities. The thing is, error objects,
like smart pointers and function objects, need to be as frictionless as
possible when crossing API boundaries, and policy-based designs work
directly against that goal.

> Take this for what it's worth, but the earlier outcome design was a lot
> > more focused.
> You may be forgetting my initial claims of a "multi-modal" design. v1
> wore many hats. v2 has no head, so it cannot wear a hat.

I'm afraid my reading is that you're shifting important design decisions to
the user, who has (by definition) lower qualifications in the domain of
error handling. Flexibility is not always good, after all we all have a C++
compiler and can handle errors with all the flexibility we need -- without
an error handling library.

> > 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.
> That was purely a simplifying narrative which was taken due to
> continuing Reddit confusion. v1 always could do far more that the
> tutorial suggested. You could, in fact, customise any of the types to
> anything you liked so long as you met an error_code or an exception_ptr
> contract.

I know. That makes sense, it's crystal clear.

> v2 now concepts matches instead. If you feed it a type matching an
> error_code concept, it treats it as an error code, otherwise it does
> not. Same for exception_ptr. Thus `outcome<int, int, int>` is legal, but
> unusable, because you cannot construct one without resorting to UB.

Assuming we agree that passing int for the P makes no sense at all:

1. Could you show a practical case that illustrates the need for using
anything but std::exception_ptr for P?

2. What is the benefit of P being semantically different from S? In other
words, what is the difference/benefit of outcome<T,S,P> over

> > 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.
> v2 was designed to dovetail into Expected neatly. It's basically a
> hugely simplified and thus much faster to compile subset. That should
> allow Expected to take on much more monadic stuff, if Vicente prefers.
> > 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.
> In the default configuration, outcome<T, EC, E|P> is only strictly
> value-or-error, value-or-exception, or value-or-error+exception.

What other options are out there? What could I do with a struct with 3
members, which outcome "strictly" forbids?

> > 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?
> It's a low level subset. Struct-based storage, not variant-based. Fast.
> Lightweight. ABI stable. But not rich, it's a barebones type.

Are you claiming that outcome<T,S,P> is faster than expected<T,S,P>? I
don't see how could that be true. Perhaps I'm missing something.

I'm also puzzled by the ABI stability claim. I mean, policy-based designs
are only ABI-stable if most users use the same policies, which obviously
defies the point of using policies.

> > 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.
> v2 is designed to be subclassed into localised implementations in a
> local namespace, which is a new thing. So it's very easy to add
> additional behaviours and features to your localised implementation,
> whilst retaining the cross-ABI interoperability between many localised
> implementations.
> v2 only provides the raw building block. What you do with it after it
> totally up to you.

The question is what's the value in using Outcome as a building block? Why
would anyone bother, if using Outcome they're basically free to do whatever?

> For example, one could build your Noexcept library
> with it quite easily, and thus "plug in" to Expected, P0650 etc.

I think Noexcept is a lot more lightweight than you think. The internal
machinery it is based on (what I think you imagine your building block
would replace) is about 500 lines, out of about 800.

Boost list run by bdawes at, gregod at, cpdaniel at, john at