Boost logo

Boost :

Subject: Re: [boost] Boost.Outcome review - First questions
From: Vicente J. Botet Escriba (vicente.botet_at_[hidden])
Date: 2017-05-23 18:58:07


Le 23/05/2017 à 16:37, Niall Douglas via Boost a écrit :
>>> 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.
How outcome ovoids this copying?
>
>> 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.
And then?

>
>>> 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.
Glad to hear that :)
> 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.
Well, all this depends on how many people is contributing to the
proposal. If we are able to work all on the same direction we can add
the efforts and arrive sonner.
Anyway, you can start playing with it using my std_make repository. If
you find that there is something missing or erroneous please create an
issue.
>
> 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.
I believe that my library is not ready to be used, not because it
doesn't exists, but because I want to be free to change it when a better
design is found.
>
>>> 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.
Glad to see that. What about expected<T,E>?
>
>>> 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.
So you are comparing to the use of exceptions here and not to the use of
output error codes?
>
>> 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().
This is similar to what BOOST_THROW_EXCEPTION does, isn't it?
Why re-invent the wheel?
> 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.
There are several version of Outcome?
>
>>> 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.
While this could be a quality for a standalone library, this is not a
quality for a Boost library.
>
> 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.
It appears on the documentation :(
>
>>> 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.
Add it on an Extension section.
>
>>> (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.
>
No this example doesn't show any advantage of Boost.Outcome, quite the
opposite. However the other example that Andrzej has posted recently is
a better example as I have already said as a response.

Best,
Vicente


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