Boost logo

Boost :

Subject: [boost] Outcome v2 (was: Re: Error handling libraries and proposals)
From: Niall Douglas (s_sourceforge_at_[hidden])
Date: 2017-07-11 08:08:20


> 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?

You wouldn't, as the voided types are not particularly useful.

However as a fundamental vocabulary type highly useful in constexpr
metaprogramming, being able to be configured with void is flexible. It
can also be useful in internal macro boilerplate. And finally, Expected
permits expected<void, void>, and seeing as I intend to offer explicit
construction from any expected concept matching type, I need to support
that as well.

> 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.

Completely agreed for a std::result<T, EC>, and any upcoming WG21 paper
will have no NoValuePolicy template parameter. For a Boost library, it's
an extended customisation point. Indeed, outcome<> actually reuses 90%
of result<>'s implementation by supplying an outcome-ish no-value
policy, so it saves me a ton of maintenance and copy and paste as well.

So I'm calling it an internal implementation detail. Not to be worried
about. And I agree with the general point made.

> > 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.

v1 Outcome's core type was `template<class Policy> class basic_monad`.
Everything user facing was typedefed to that with a default choice of
policy.

v2 similarly defaults the choice of policy. v2 is neither more nor less
flexible. End users get the same degree of customisation power. Whether
they use it wisely or not is up to them, as a vocabulary type it's not
my remit to enforce good behaviour by library developers, just to
encourage it. If they want to shoot themselves in the foot, they can.

> 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:

Not at all. There is nothing wrong with int as either an exception or a
payload type. Some end user may want that for some reason. For the exact
same reason, you can throw an int in C++. It can make sense.

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

The idea behind P is that when trait::is_exception_ptr<P> is false, then
P is a payload type which provides additional information for your main
EC failure type. So:

outcome<Success, Failure, FailureInfo>

Or more concretely:

outcome<handle, std::error_code, std::filesystem::path>

... could return an open file handle on success, or an error code plus
the failing path on failure.

> 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
> expected<T,S,P>?

Peter Dimov wanted the ability for the Filesystem TS to return the
exception which would have been thrown in the error_code& overloads, so
calling code can effectively filter whether to throw the exception or
not. I can see that as a pretty useful use case, albeit expensive seeing
as an exception_ptr is expensive to allocate. But still a reasonable
half way house between a full throw-catch cycle and the currently
metadata impoverished error_code& overloads.

So outcome<handle, std::error_code, std::exception_ptr> does permit an
error_code+exception_ptr construction, and you get exactly the desired
functionality.

> > 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?

Things you can't do:

* value + error
* value + exception
* value + error + exception

> > 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.

It should be to compile. Outcome is essentially nothing :)

> 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.

Remember Outcome permutes its namespace with the git SHA. So it's
effectively randomised, and thus no symbol collisions can occur.

The only ABI instability is thus the data layout, and as I've mentioned
I've deliberately kept result's data layout C-struct compatible.

> > 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?

Using your logic, why use std::pair or std::tuple when a struct will do?
Same argument.

> 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.

As you saw on SG14, using any black box library routines is always going
to be a problem for the fixed latency user base. Your library is shorter
than mine, but it calls unknown latency routines. Mine never does.

You also impose TLS on your end users. Mine is much lower level than
that, if end users wish to combine mine with TLS to achieve your
library, they'll find it very easy. So why innovate for them?

Niall

-- 
ned Productions Limited Consulting
http://www.nedproductions.biz/ http://ie.linkedin.com/in/nialldouglas/

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