Boost logo

Boost :

Subject: Re: [boost] [review] Review of Outcome v2 (Fri-19-Jan to Sun-28-Jan, 2018)
From: Emil Dotchevski (emildotchevski_at_[hidden])
Date: 2018-01-31 02:52:37


On Tue, Jan 30, 2018 at 4:40 PM, Niall Douglas via Boost <
boost_at_[hidden]> wrote:

> > The policy-based design is especially problematic because of the
> expressed
> > desire to provide interoperability between diverse APIs each using
> > different error reporting. This is a bit counter-intuitive, because the
> > natural inclination is to provide maximum flexibility, but in this case
> > other considerations are more important. There is a reason why in C
> > libraries the default error reporting mechanism is to return int, even
> > though the language does permit programmers to return structs, which
> would
> > be more flexible.
>
> As the last section of the tutorial covers, there is a
> non-source-intrusive mechanism for externally specifying interoperation
> rules to handle libraries using one policy interoperating with libraries
> with different policies. This lets Eve stitch together the Alice and Bob
> libraries without having to modify their source code, and without
> affecting any other libraries. I personally think this Outcome's coup de
> grace and why it's the only scalable choice for large programs
> considering using this sort of error handling.
>

This is wishful thinking. Imagine someone wanted to create a library that
enabled all the different string types used in various libraries to get
seamlessly stitched together. I'm sure that's possible, but I don't think
it's a good idea. Ultimately, you pass char const * if you want to be
compatible. Not ideal, not always possible, but that's how things are
sometimes.

It is the same with error handling: if you want to facilitate
inteoperability, you'd return an int. Not ideal, but it's still the best
option for interoperability.

In addition, semantically policies don't make sense to me. For example, to
throw or not to throw can not be a matter of "policy" because that makes
throwing completely pointless.

> > [snip why Emil loves exceptions]
> > So, the design of "an error handling library that does not use C++
> > exceptions" has to target the tricky bits where exceptions would be
> > annoying or can't be used, not the general case where they can.
>
> I get Emil that you love exceptions. So you won't like the whole premise
> behind Outcome, indeed you wrote an alternative to Outcome v1 last year
> to prove the pointlessness of the Outcome premise.
>

I wanted to be fair and refrained from mentioning (Boost) Noexcept but the
reason why I wrote it was not to prove Outcome is pointless, but because I
saw the point in Outcome, disagreed with the design, yet I had nothing
tangible to prove even to myself that another approach is preferable (and
there are problems where the best solution is not ideal.)

It's not that I love exceptions, but that there is no other way to enforce
postconditions and, like I already pointed out, postconditions are an
integral part of the object encapsulation model in C++. Besides, there is
*nothing* good about having to write:

f1();
if( error ) return error;
f2();
if( error ) return error;

More importantly, there are *no* technical reasons why this should be slow
when done automatically by the compiler. Literally, the people who insist
on using OUTCOME_TRY instead of throwing exceptions have no reason to want
that, except for lack of better optimizers (and even then, they have no
evidence that the current state of affairs is "too slow").

I would very much like to be proven wrong about this, that there is
*nothing* inherently slow in exception handling. It would save me a lot of
energy. :)

It is this part of Outcome, where it is trying to solve a problem much
better solved by exception handling, that I find pointless, except in cases
where exception handling is off limits, for example when errors need to
pass through third-party code that is not exception-safe, in particular
when errors need to be transported across C code.

But Outcome does nothng to solve this problem.

In addition, I don't find it pointless to postpone or avoid exceptions
being thrown. Writing:

optional<int> value;
try
{
  value = foo();
}
catch( error & )
{
}
if( value )
  use_value(value);

is cumbersome. It is better to be able to say

if( result<int> value=foo() )
  use_value(value.get());

But this is not the "stitch together all the error handling object types in
the universe" problem.

> Speaking of interoperability, it is especially tricky to report errors
> from
> > C-style callbacks. This would be nice to support because C++ exceptions
> are
> > off-limits in this case, yet it is sometimes desirable to communicate
> > user-specific information across the third-party C callback mechanism
> (this
> > is sometimes supported by a void * user data pointer, but that is not
> > always the case).
> >
> > The kind of objects that can survive crossing API boundaries would be of
> > basic types, or have one or two members of basic types. Think
> > std::shared_ptr<T>, which _always_ consists of two pointers, rather than
> > SmartPtr<T,Policy1,Policy2> which consist of who knows what. Note that
> this
> > is not the same problem as "using result<T> from C code", which Outcome
> > allows. The important question is not how do I use result<T> from C, but
> > how do I transport result<T> across a third party context which knows
> > nothing about Outcome (as a side note, C++ exceptions also cause issues
> > when crossing API boundaries, even if we set exception safety
> requirements
> > aside.)
>
> How do you transport any type across a third party context which knows
> nothing about that type?
>
> There is a long list of standard techniques. Outcome is just another
> C++/C type, and any of those techniques apply just the same to it for
> that problem solution.
>
> I don't see the issue you're making here.
>

Are you saying that you recommend returning shared_ptr<result<T> >? :)

The issue I'm making is that Outcome types should as much as possible be
designed to work well when crossing API boundaries. Granted, there is a
competing design goal of flexibility, hence the design rationale behind
(Boost) Noexcept.

But lots of people don't agree with you, and they want something like
> this in C++.
>

If you mean that many people hold unsubstantiated beliefs and want
something, that's just life. :)

> > Finally, I'll point out that a lot of the positive feedback comes from
> > people who think that it is a good idea to replicate the Rust error
> > reporting mechanism in C++. This seems to be an axiomatic belief, since
> > I've never seen anyone attempt to substantiate it. This is important, as
> it
> > is typical for programmers coming from other languages to be shocked by
> > various C++ language features, and this should not be confused with
> > problems in the C++ language specification.
>
> If this was being forced on all users across the board, I can see you
> might have a point.
>
> But this is an opt in library, and nobody is claiming nor pretending
> that anything but a minority will ever want to use it. Using Outcome in
> your code does come with costs in terms of maintenance and learning
> curve. Most C++ users will never want nor need it.
>

Obviously, I'm not against people using Outcome, not that anyone would care
what I think.

> > - What is your evaluation of the implementation?
> >
> > Lack of C++11 support could be problematic. The use of macros to
> > disambiguate namespace is cumbersome. Overall the library relies heavily
> on
> > macros, which is not a good thing for a C++ library.
>
> I have no idea where you get the idea that the library relies heavily on
> macros.
>
> The only obligatory macro is BOOST_OUTCOME_V2_NAMESPACE. All the rest
> are optional.
>
> And as I've explained several times now, the permuting namespace is to
> force DLLs built against version X of Outcome to interoperate with other
> DLLs built against Y of Outcome via the ValueOrError Concept interface.
>
> I'd have thought Emil given your experience in games that you'd
> understand the importance of stable ABI guarantees. This makes stable
> ABI guarantees possible for the standalone Outcome.
>

How does that benefit Boost users?

But you have a point that most macros are dealing with using Outcome from
C, my bad.

> Now, any Boost.Outcome would eventually decay its macro into
> boost::outcome::v2 once a final v2 has arrived. That's not a v2 design,
> that's a v2 *implementation*. I'll be running the ABI Compliance Checker
> per commit once v2 is decided upon to ensure it remains stable.
>

v2 of Boost Outcome or standalone Outcome?

>
> > It seems like Outcome wants to stay away from Boost. By that I mean that
> > great care has been taken to decouple it from Boost, and I sense that
> this
> > desire comes from the target audience: the so-called "low latency" crowd
> > wouldn't touch Boost with a 9 foot pole. While it is generally a bad idea
> > to speculate about such things, it seems to me the motivation for
> > submitting Outcome for a Boost review is not to benefit the Boost
> > community; for us the coupling with Boost is not a problem.
>
> The decoupling is purely for ease of maintenance. I can script patchsets
> from a Boost.Outcome into Outcome, and vice versa. I am sorry that you
> have read ulterior motives into what is an engineering choice.
>

That's why I said it is a speculation. But this still leaves me wondering
which one is the real Outcome and why are there two? Is the intention to
deprecate standalone Outcome?

Again, this is what makes stable ABI guarantees possible. I'm amazed
> that you don't get this.
>

You're looking at the problem of ABI compatibility as "what do we have to
do to get this complex type passed across a DLL boundary correctly."

I'm looking at it as "how should we design this type so that ABIs don't
break as much". Policies (much less user-defined policies) don't fit into
this view.

Emil


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