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
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
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
, 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
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 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
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
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
. But as soon as you roll expected 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/