Boost logo

Boost :

Subject: Re: [boost] Boost.Outcome review - First questions
From: Niall Douglas (s_sourceforge_at_[hidden])
Date: 2017-05-23 14:37:12


>> Outcome's Expected provides both a subset and a superset of your
>> Expected proposal.
>>
>> I have promised to track closely your proposal paper, but I have no
>> interest in providing a perfect match to your proposal. Outcome lets you
>> seamlessly mix expected<T, E> with Outcomes and with *any arbitrary
>> third party error handling system* thanks to the policy based core
>> implementation. It therefore cannot exactly implement your proposal, it
>> needs to differ because it *is* different. My claim is that any code
>> written to use LEWG Expected will work exactly the same with Outcome's
>> Expected. If it does not, I will repair Outcome's Expected until code
>> using it works identically. I think this a very reasonable position to
>> take, especially as you are still changing the proposed Expected.
>
> You are right it is moving and not yet accepted.
> One of the major interest I have in this review is to try to see what
> can be improved on the proposed std expected.

I concur. If someone comes along with an obviously superior design to
both Expected and Outcome, that would be an enormous win.

(and sorry Peter, your expected<T, E...> design I am not persuaded by,
but perhaps I am overestimating the brittle coupling generated by
allowing every possible domain specific error type to bubble up to high
level code)

>> Both you and Vinnie have called Outcome "over engineered". I
>> respectfully suggest neither of you understands the purpose of Outcome
>> the **framework** which is to provide a very low overhead universal
>> error handling framework. That's why there is the exact same CRTP policy
>> based basic_monad class being typedefed into the aliases expected<T, E>,
>> outcome<T>, result<T>, option<T>. They are all the same class and
>> object, just with policy-determined "personality" that lets them provide
>> differing semantics to each user, yet they all still operate seamlessly
>> together and can be fed into one another e.g. via the TRY operation, and
>> with very low, usually minimum, runtime overhead and low compile time
>> overhead.
>
> Okay. Has this clearly stated and showed throw examples in the
> documentation? Sorry, I have not read all the documentation yet.

The landing page for the docs states the above and gives a motivating
sample of code to show it. I don't know what else I can say to
communicate this.

> IIUC, what you want is to have outcomes that copy/move well each one on
> each other.

That was exactly a motivating reason for choosing this design
originally. Having the same base storage implementation should allow the
compiler to completely eliminate memory copying to implement layout
changes when a personality is changed.

> Is this the major drawback you find to the use of
> std::expected and std::optional?

No, not at all. Very recent optimisers e.g. in better than clang 4.0 and
in GCC 6.0 with -O3 optimisation turn out to have rendered the common
storage design choice no longer appropriate. But that wasn't the case
when I began Outcome two years ago.

>> If you are only in the market for just an expected<T, E> implementation
>> and nothing else, then yes Outcome looks over engineered. But my claim
>> is that in any real world code base of any size, people end up having to
>> add layers on top of expected<T, E> etc to aid interop between parts of
>> a large code base.
>
> I recognize there is a problem when we need to forwarding errors that
> are transformed.
> This is my TODO plan for the standard proposals. Maybe my approach would
> be what you consider is the way we shouldn't follow. We will see.

Actually I think that your proposal for this is very interesting with a
lot of potential. Yours is much more powerful than the very limited,
unambitious, almost simple "intrusive" interop that I've chosen.

But your proposal is some years away from being production ready I
think. There are lots of corner cases and quandaries which need to be
resolved before people should start using it in code intended for long
term usage.

Between now and then, here is Outcome. It is an impoverished experience
compared to your proposal, no doubt. But it has the advantage of
relative simplicity and it's ready for use now, not later.

>> They will have varying degrees of success, as people
>> on Reddit have told me regarding their local expected<T, E>
>> implementations. A lot of people end up with macros containing switch or
>> try catch statements to map between differing error handling systems.
>> And that is bad design.
>
> This transformation can be hidden by higher level abstractions, but IMO
> they should be invoked explicitly.

I agree that a user explicitly chooses to use result<T> instead of an
expected<T> to explicitly opt into default actions to save boilerplate.
That's an explicit choice of a higher level abstraction.

>> I will claim that if you *are* building such an interop framework, you
>> will find that Outcome is the bare minimum implementation possible.
>
> Maybe or maybe not. If outcome uses more storage than expected or
> optional it is not the base minimum.

As already described, Outcome consumes minimum overhead. To prove this I
wrote this small program on godbolt:

    printf("%d\n", sizeof(std::optional<char>));
    printf("%d\n", sizeof(std::optional<int>));
    printf("%d\n", sizeof(std::optional<size_t>));

The answers are 2, 8 and 16 bytes.

I wrote this just there in Outcome:

  printf("%d\n", sizeof(option<char>));
  printf("%d\n", sizeof(option<int>));
  printf("%d\n", sizeof(option<size_t>));

The answers are 2, 8 and 16 bytes. Identical overhead.

>> It
>> is, if anything, *under* engineered compared to the many other
>> "universal error handling frameworks for C++" out there which tend to
>> throw memory allocation and smart pointers and type erasure at the
>> problem.
>
> Niall sorry, I don't like the worlds "universal" and similar
> qualifications as "ultra-lightweight error handling" and "minimum
> overhead universal outcome transport mechanism for C++"
> To what other "universal error handling are you referring?
> Ho wis your design universal?

You could wholly replace all usage of C++ exceptions with Outcome
without losing fidelity of error information.

(I wouldn't advise that you should, but you can)

You would thus exchange (slightly) worse performance of successful code
in exchange for vastly better and predictable performance of
unsuccessful code.

Note that the cost of throwing and catching a C++ exception with a table
based EH C++ compiler is highly unpredictable, and costs between
1500-3000 CPU cycles per stack frame unwound between the try and catch
site. You can find the benchmark in the benchmark directory in Outcome's
git repo.

> What is the behavior on the absence of exception on the functions that
> throw exceptions? Are these functions disabled? Does the fucntion
> terminate? Calls a handler?

All exceptions Outcome ever throws are done via a user redefinable
macro. Those macros are listed at
https://ned14.github.io/boost.outcome/md_doc_md_04-tutorial_c.html.

If C++ exceptions are enabled, the default macro definition throws the
exception.

If C++ exceptions are disabled, the default macro definition prints a
descriptive message and a stacktrace to stderr and calls std::terminate().

The unit test suite is compiled with C++ exceptions disabled and
executed per commit by both Travis and Appveyor to make sure all the
conformance and behaviours still work correctly.

>> is an
>> excellent neighbour to all other C++ libraries
>
> as for example?

It pollutes no namespaces, interferes in no way with other C++ including
other versions of itself. You can mix multiple versions of Outcome in
the same binary safely.

>> AND build systems,
>
> I'm less concerned by the build systems, but could you elaborate?

Outcome doesn't require any magic macros predefined and can be used by
end users simply by dropping a tarball of the library into their source
code and getting to work.

Outcome is my first non-toy library to not require non-C++ tooling to be
built. It uses the preprocessor instead.

If you are cmake based, Outcome's cmake is modern cmake 3 throughout and
ticks every box in how modern cmake should be designed and written and
consumed by arbitrary third party cmake. It lacks some cmake support
like for the cmake package registry and (still!) make install, but the
reason I haven't fixed those yet is that cmake usage is the enormously easy:

add_subdirectory(
  "${CMAKE_CURRENT_SOURCE_DIR}/boost.outcome" # path to outcome source
  "${CMAKE_CURRENT_BINARY_DIR}/boost.outcome" # your choice of where to
put binaries
  EXCLUDE_FROM_ALL # please only lazy build
outcome on demand
)
target_link_libraries(myexe PRIVATE boost::outcome::hl)

No messing about with include paths, compiler flags, macros, reading
documentation or anything. The above also only lazy builds the parts of
Outcome used by your cmake projects on demand, and doesn't clutter
generated IDE project files with anything but the minimum.

Modern cmake is so amazingly better than v2 cmake. Stephen Kelly was one
of the main people responsible for these improvements, he did a great
job there.

>> and
>> avoids where possible imposing any constraints on the user supplied
>> types fed to it. It also lets you use as much or as little of itself as
>> you choose.
>
> It is not imposing the use of boost::outcome?

Not what I meant. I meant Outcome is designed so you can use parts of it
without being obliged to use all of it.

>> So okay, it's over engineered if you think you want just an expected<T,
>> E>. But as soon as you roll expected<T, E> out into your code, you are
>> going to find it won't be enough. Thus the rest of Outcome comes into
>> play, and even then there are small gaps in what Outcome provides which
>> any real world application will still need to fill. That's deliberate
>> *under* engineering, I couldn't decide on what was best for everyone, so
>> I give the end user the choice by providing many customisation points
>> and macro hooks.
>
> I must recognize I'm reluctant to the basic_monad abstraction, and of
> course if the library is accepted should change of name.

I've already removed "monad" from all the documented types making up the
public API in develop branch.

Changing basic_monad is a week or two of work because it must be done by
hand, I cannot automate the name change. So I leave it until after the
review. If Outcome is rejected I won't bother wasting so much time.
After all, basic_monad is not a public facing type, it only appears in
the debugger. And the name is just a bunch of ASCII characters.

>> and I appreciate that the docs do not sufficiently
>> get into the universal error handling framework part of Outcome. That is
>> due to repeated Reddit feedback telling me that earlier editions of the
>> docs didn't make sense, and I needed to go much slower and hold the
>> hand, so you got the current tutorial which as you've already observed,
>> is too long as it is. It would become even longer if you started
>> dissecting universal error handling strategies.
> If the main goal is the universal error handling strategies, then the
> documentation must describe what this is?

That's a very nebulous topic to discuss in documentation. Look at this
discussion thread between me and you over this past week. Imagine
summarising that into documentation that both you and I agree with, AND
is intelligible to the average programmer, AND does not form a document
sized like a small book.

Very, very hard.

>> (And my thanks to Andrzej for the motivating example on the landing page
>> of the docs, he did a great job capturing the universal error handling
>> framework aspect of Outcome. I would love to know if reviewers can make
>> sense of it, or did it just confuse the hell out of everybody)
>>
> Could you tell us which example?

It's the landing page motivating code example. Bottom of
https://ned14.github.io/boost.outcome/index.html.

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