[review] **NEXT WEEK** Review of Outcome (starts Fri-19-May)
Hi Everyone,
** HEADS UP, NEXT WEEK **
The formal review of Niall Douglas' Outcome library starts next week
(Fri-19-May to Sun-28-May).
Your participation is encouraged, as the proposed library is uncoupled and
focused, and reviewers don't need to be domain experts to appreciate the
potential usefulness of the library and to propose improvements. Everyone
needs (and has suffered) error handling, and can compose an opinion on that
topic.
Outcome is a header-only C++14 library providing expressive and type-safe
ultra-lightweight error handling, suitable for low-latency code bases.
Key features:
*- Makes using std::error_code from C++11's
How does expected work in a world of multiple return value, structured bindings?
________________________________
From: Boost
On 12/05/2017 14:13, Jarrad Waterloo via Boost wrote:
How does expected work in a world of multiple return value, structured bindings?
I see no reason that they shouldn't work perfectly. Expected provides lvalue, const lvalue, rvalue and const rvalue observers. So it should just work. But I'll admit I haven't tested it. Can you suggest a use case which you would like to know if it works with Expected? I can throw together some code and see if it works for you. Niall -- ned Productions Limited Consulting http://www.nedproductions.biz/ http://ie.linkedin.com/in/nialldouglas/
2017-05-12 18:26 GMT+02:00 Niall Douglas via Boost
On 12/05/2017 14:13, Jarrad Waterloo via Boost wrote:
How does expected work in a world of multiple return value, structured bindings?
I see no reason that they shouldn't work perfectly. Expected provides lvalue, const lvalue, rvalue and const rvalue observers. So it should just work.
But I'll admit I haven't tested it. Can you suggest a use case which you would like to know if it works with Expected? I can throw together some code and see if it works for you.
Or maybe what Jarrad was asking was wether one could write code like this: ``` outcome<int> fun(); int main() { auto [val, err] = fun(); if (err) ... } ``` But if this was the question: outcome cannot be used in this way. For the same reason as variant<> cannot be used here: because outcome<> can store only one of - value or error code - at a time: never both. Regards, &zej;
Sounds useful. I welcome a 'expected' implementation, even if, in my testing, results from this sort of thing are highly CPU-dependent and non-intuitive. On 12/05/2017 4:19 a.m., charleyb123 . via Boost wrote:
Hi Everyone,
** HEADS UP, NEXT WEEK **
The formal review of Niall Douglas' Outcome library starts next week (Fri-19-May to Sun-28-May).
Your participation is encouraged, as the proposed library is uncoupled and focused, and reviewers don't need to be domain experts to appreciate the potential usefulness of the library and to propose improvements. Everyone needs (and has suffered) error handling, and can compose an opinion on that topic.
Outcome is a header-only C++14 library providing expressive and type-safe ultra-lightweight error handling, suitable for low-latency code bases.
Key features:
*- Makes using std::error_code from C++11's
more convenient and safe *- Provides high-quality implementation of proposed std::expected (on C++20 standards track) *- Good focus on low-latency (with tests and benchmarks) *- Error-handling algorithmic composition with-or-without C++ exceptions enabled *- No dependencies (not even on Boost) This review is timely, as C++17 brings us std::optional<T>. The upcoming std::expected
(an implementation provided in Outcome) is a generalization of std::optional<T> that provides a value, where the unhappy result is a 'std::error_code' or an instance of "your-chosen-error-type". The library further provides 'outcome
' for handling to safely wrap throwing APIs. Documentation: https://ned14.github.io/boost.outcome/index.html
ACCU 2017 talk including design rationale: https://www.youtube.com/watch?v=XVofgKH-uu4
GitHub: https://github.com/ned14/boost.outcome
Latest tarball: https://github.com/ned14/boost.outcome/releases/ download/boost_peer_review3/boost.outcome-v1.0-source-201705111650.tar.xz
Note: Tarball might be easiest; but if you want to clone from GitHub directly, after the clone you should run the following command to get the source zip exactly: git submodule update --init --recursive
NEXT WEEK (when the public review is started): Please post your comments and review to the boost mailing list (preferably), or privately to the Review Manager (to me ;-). Here are some questions you might want to answer in your review:
- What is your evaluation of the design?
- What is your evaluation of the implementation?
- What is your evaluation of the documentation?
- What is your evaluation of the potential usefulness of the library?
- Did you try to use the library? With what compiler? Did you have any problems?
- How much effort did you put into your evaluation? A glance? A quick reading? In-depth study?
- Are you knowledgeable about the problem domain?
And most importantly:
- Do you think the library should be accepted as a Boost library?
For more information about Boost Formal Review Process, see: http://www.boost.org/community/reviews.html
Thank you very much for your time and efforts.
--charley
_______________________________________________ Unsubscribe & other changes: http://lists.boost.org/mailman/listinfo.cgi/boost
On 13/05/2017 00:53, Soul Studios via Boost wrote:
Sounds useful. I welcome a 'expected' implementation, even if, in my testing, results from this sort of thing are highly CPU-dependent and non-intuitive.
Can you clarify? Niall -- ned Productions Limited Consulting http://www.nedproductions.biz/ http://ie.linkedin.com/in/nialldouglas/
My apologies, when my brain saw "expected" it also saw "__builtin_expect" and I immediately assumed an unlikely/likely proposal - of course that's not what it is. My bad :) On 13/05/2017 12:17 p.m., Niall Douglas via Boost wrote:
On 13/05/2017 00:53, Soul Studios via Boost wrote:
Sounds useful. I welcome a 'expected' implementation, even if, in my testing, results from this sort of thing are highly CPU-dependent and non-intuitive.
Can you clarify?
Niall
On Sat, May 13, 2017 at 5:00 PM, Soul Studios via Boost
My apologies, when my brain saw "expected" it also saw "__builtin_expect" and I immediately assumed an unlikely/likely proposal
If you are referring to a cross-platform implementation of __builtin_expect that uses regular C++ then you have my full support, you should propose such a library. As a Visual C++ user I would find that very helpful!
On 14/05/2017 01:41, Vinnie Falco via Boost wrote:
On Sat, May 13, 2017 at 5:00 PM, Soul Studios via Boost
wrote: My apologies, when my brain saw "expected" it also saw "__builtin_expect" and I immediately assumed an unlikely/likely proposal
Ah, okay thanks.
If you are referring to a cross-platform implementation of __builtin_expect that uses regular C++ then you have my full support, you should propose such a library. As a Visual C++ user I would find that very helpful!
There was a very interesting talk at ACCU (unfortunately in the non-videoed room) by a HFT guy explaining all the ways in which recent GCC versions have bugged __builtin_expect, like inverting the code path you specifically told the compiler was the hot path. GCC devs apparently don't care enough to fix it despite multiple persistent reports, and the feature is now useless on GCC >= 5. The speaker recommended clang, especially very recent clang which is apparently finally being competitive with GCC in generating very tight code. I'd back that up, clang 5.0 is generating much tighter code with Outcome than clang 3.x did, markedly so. But I can see a feature like __builtin_expect that going the way of the dodo as the compiler vendors really would prefer if you used profile guided optimisation instead. Passing that sort of micro-info from the parser to the backend is surely complex to get right. Niall -- ned Productions Limited Consulting http://www.nedproductions.biz/ http://ie.linkedin.com/in/nialldouglas/
On Sun, 14 May 2017, Niall Douglas via Boost wrote:
There was a very interesting talk at ACCU (unfortunately in the non-videoed room) by a HFT guy explaining all the ways in which recent GCC versions have bugged __builtin_expect, like inverting the code path you specifically told the compiler was the hot path.
GCC devs apparently don't care enough to fix it despite multiple persistent reports, and the feature is now useless on GCC >= 5. The speaker recommended clang, especially very recent clang which is apparently finally being competitive with GCC in generating very tight code. I'd back that up, clang 5.0 is generating much tighter code with Outcome than clang 3.x did, markedly so.
But I can see a feature like __builtin_expect that going the way of the dodo as the compiler vendors really would prefer if you used profile guided optimisation instead. Passing that sort of micro-info from the parser to the backend is surely complex to get right.
I looked for bug reports about __builtin_expect in gcc's bugzilla, and the only relevant one I found was https://gcc.gnu.org/bugzilla/show_bug.cgi?id=59521 about using it in a switch. Could you back your statement with some links? Otherwise it sounds like FUD (at least the part about ignoring multiple persistent reports). -- Marc Glisse
But I can see a feature like __builtin_expect that going the way of the dodo as the compiler vendors really would prefer if you used profile guided optimisation instead. Passing that sort of micro-info from the parser to the backend is surely complex to get right.
I looked for bug reports about __builtin_expect in gcc's bugzilla, and the only relevant one I found was https://gcc.gnu.org/bugzilla/show_bug.cgi?id=59521 about using it in a switch. Could you back your statement with some links? Otherwise it sounds like FUD (at least the part about ignoring multiple persistent reports).
It's possible my memory is faulty, but I don't think it was. It was a very good talk, basically a long list with extensive microdetail of how compiler version quirks get in the way of micro optimisation. Builtin expect was but one of many in a long list. The presenter, Jason McGuiness, is one of the more colourful regular attendees at ACCU and from all my interactions with him to date, I would be considering him to not be FUDing. He's presented similar material at ACCU London talks and several others. His slides for the ACCU talk aren't online yet, so I'm sent him a LinkedIn request and I'll ask for a copy. I'll post a link here if I get them. Niall -- ned Productions Limited Consulting http://www.nedproductions.biz/ http://ie.linkedin.com/in/nialldouglas/
On 14/05/2017 12:33, Niall Douglas wrote:
But I can see a feature like __builtin_expect that going the way of the dodo as the compiler vendors really would prefer if you used profile guided optimisation instead. Passing that sort of micro-info from the parser to the backend is surely complex to get right.
I looked for bug reports about __builtin_expect in gcc's bugzilla, and the only relevant one I found was https://gcc.gnu.org/bugzilla/show_bug.cgi?id=59521 about using it in a switch. Could you back your statement with some links? Otherwise it sounds like FUD (at least the part about ignoring multiple persistent reports).
It's possible my memory is faulty, but I don't think it was. It was a very good talk, basically a long list with extensive microdetail of how compiler version quirks get in the way of micro optimisation. Builtin expect was but one of many in a long list.
The presenter, Jason McGuiness, is one of the more colourful regular attendees at ACCU and from all my interactions with him to date, I would be considering him to not be FUDing. He's presented similar material at ACCU London talks and several others.
His slides for the ACCU talk aren't online yet, so I'm sent him a LinkedIn request and I'll ask for a copy. I'll post a link here if I get them.
Jason has provided a copy of the ACCU talk slides at http://research.ma.cx/ACCU_Conference_2017_v1_1.pdf. Lots of graphs and assembler showing builtin_expect not working right on GCC. Niall -- ned Productions Limited Consulting http://www.nedproductions.biz/ http://ie.linkedin.com/in/nialldouglas/
On Mon, 15 May 2017, Niall Douglas via Boost wrote:
On 14/05/2017 12:33, Niall Douglas wrote:
But I can see a feature like __builtin_expect that going the way of the dodo as the compiler vendors really would prefer if you used profile guided optimisation instead. Passing that sort of micro-info from the parser to the backend is surely complex to get right.
I looked for bug reports about __builtin_expect in gcc's bugzilla, and the only relevant one I found was https://gcc.gnu.org/bugzilla/show_bug.cgi?id=59521 about using it in a switch. Could you back your statement with some links? Otherwise it sounds like FUD (at least the part about ignoring multiple persistent reports).
It's possible my memory is faulty, but I don't think it was. It was a very good talk, basically a long list with extensive microdetail of how compiler version quirks get in the way of micro optimisation. Builtin expect was but one of many in a long list.
The presenter, Jason McGuiness, is one of the more colourful regular attendees at ACCU and from all my interactions with him to date, I would be considering him to not be FUDing. He's presented similar material at ACCU London talks and several others.
His slides for the ACCU talk aren't online yet, so I'm sent him a LinkedIn request and I'll ask for a copy. I'll post a link here if I get them.
Jason has provided a copy of the ACCU talk slides at http://research.ma.cx/ACCU_Conference_2017_v1_1.pdf.
Thanks.
Lots of graphs and assembler showing builtin_expect not working right on GCC.
Not really. There are only 2 mentions of __builtin_expect in those slides: * switch expansion in gcc ignores __builtin_expect (that's the PR I referenced above) * an unclear benchmark about adding __builtin_expect in 1 place in some unknown code, where __builtin_expect actually seems to help with the more recent version of gcc. No "inverting the code path you specifically told the compiler was the hot path", no "multiple persistent reports", no "the feature is now useless on GCC >= 5" (he actually writes "Newer versions of g++ make better use of optimization" though that's not specifically about __builtin_expect), no "The speaker recommended clang, especially very recent clang" ("No one compiler appears to be best - choice is crucial. Newest versions of clang have not been investigated.") The vocabulary in the slides is odd (any memory is called stack, anything 64 bits is called SSE) and there are some questionable statements. Taking a gcc dev point of view, the main information I got form those slides is interest in PR 59521 and some small margin of progress on copying constant strings. If anything, the section on static branch prediction advertises that one should use __builtin_expect more ;-) -- Marc Glisse
The vocabulary in the slides is odd (any memory is called stack, anything 64 bits is called SSE) and there are some questionable statements. Taking a gcc dev point of view, the main information I got form those slides is interest in PR 59521 and some small margin of progress on copying constant strings.
If anything, the section on static branch prediction advertises that one should use __builtin_expect more ;-)
I am not the author of the talk. I can say he checked the contents of this thread and had a few colourful things to say about it, but did not disagree with my recollection of what he said. As I mentioned before, he's a long time attendee and presenter at ACCU. I am inclined to believe whatever he claims, he knows his stuff. Niall -- ned Productions Limited Consulting http://www.nedproductions.biz/ http://ie.linkedin.com/in/nialldouglas/
On Mon, 15 May 2017, Niall Douglas via Boost wrote:
The vocabulary in the slides is odd (any memory is called stack, anything 64 bits is called SSE) and there are some questionable statements. Taking a gcc dev point of view, the main information I got form those slides is interest in PR 59521 and some small margin of progress on copying constant strings.
If anything, the section on static branch prediction advertises that one should use __builtin_expect more ;-)
I am not the author of the talk. I can say he checked the contents of this thread and had a few colourful things to say about it, but did not disagree with my recollection of what he said.
As I mentioned before, he's a long time attendee and presenter at ACCU. I am inclined to believe whatever he claims, he knows his stuff.
Ok, if what he said is different from what he wrote in the slides, all I can do is trust you... Thanks again for forwarding the link to the slides, -- Marc Glisse
If you are referring to a cross-platform implementation of __builtin_expect that uses regular C++ then you have my full support, you should propose such a library. As a Visual C++ user I would find that very helpful!
https://groups.google.com/a/isocpp.org/forum/#!searchin/sg14/likely/sg14/jU8... There's one here. I don't think VCC supports any form of real branch hinting at present though :)
There was a very interesting talk at ACCU (unfortunately in the non-videoed room) by a HFT guy explaining all the ways in which recent GCC versions have bugged __builtin_expect, like inverting the code path you specifically told the compiler was the hot path.
That kind of thing certainly matches my experience of using it - there are situations where my code was 99.9999% hot path, but somehow utilizing builtin_expect made benchmarks worse, assumably by mucking up the optimization code.
But I can see a feature like __builtin_expect that going the way of the dodo as the compiler vendors really would prefer if you used profile guided optimisation instead. Passing that sort of micro-info from the parser to the backend is surely complex to get right.
Unfortunately that would not be kosher for fields like AAA gamedev, where you're talking massive codebases compiling into singular files where PGO is not acceptable because user behaviour is not predictable. And, in my experience, PGO success varieis wildly from compiler to compiler (VCC2013's was a pessimisation feature - even with simple, repeatable benchmark code).
Documentation: https://ned14.github.io/boost.outcome/index.html
Why does the library make the types possibly-empty? I couldn't find the answer in the documentation.
On 14/05/2017 16:31, Peter Dimov via Boost wrote:
Documentation: https://ned14.github.io/boost.outcome/index.html
Why does the library make the types possibly-empty? I couldn't find the answer in the documentation.
Do you mean empty angle brackets in the make functions? i.e. outcome::make_exceptional_outcome<>()? If so, they are superfluous and can be omitted, they were there for exposition only (see the functions docs and you'll see why). The code compiles fine without them. If you feel that they ought to be removed from the code examples in the docs, please do say so during your review, though as I mention they were deliberately left in to remind people look up the API docs. Niall -- ned Productions Limited Consulting http://www.nedproductions.biz/ http://ie.linkedin.com/in/nialldouglas/
Niall Douglas wrote:
On 14/05/2017 16:31, Peter Dimov via Boost wrote:
Documentation: https://ned14.github.io/boost.outcome/index.html
Why does the library make the types possibly-empty? I couldn't find the answer in the documentation.
Do you mean empty angle brackets in the make functions? i.e. outcome::make_exceptional_outcome<>()?
No, I mean that the types have an empty state. variant
Why does the library make the types possibly-empty? I couldn't find the > answer in the documentation.
No, I mean that the types have an empty state. variant
and expected don't have one.
C++ 17's variant<...> does actually. It's called valueless by exception.
It is hoped the valueless state may be removed from a future C++
standard, but for now it is present.
outcome<T>, result<T> and option<T> all have formal empty states as part
of their programming model. They are quite useful for a multitude of
uses, I mainly have used them to early out from a sequence of monadic
operations or to signal an abort from a noexcept function.
expected
Niall Douglas wrote:
outcome<T>, result<T> and option<T> all have formal empty states as part of their programming model. They are quite useful for a multitude of uses, I mainly have used them to early out from a sequence of monadic operations or to signal an abort from a noexcept function.
Would you mind going into more detail with respect to the use cases? What does returning an empty result mean to the caller, conceptually? The idea of these result types is to return either a value or a reason for the lack of value, and an empty state is lacking both the value and the explanation for why the value is missing.
outcome<T>, result<T> and option<T> all have formal empty states as part of their programming model. They are quite useful for a multitude of uses, I mainly have used them to early out from a sequence of monadic operations or to signal an abort from a noexcept function.
Would you mind going into more detail with respect to the use cases?
That would be tricky. The empty state is for whatever the programmer wants it to mean for some use case. It's a bit like returning a null pointer, that could mean failure, or success, or something else. Totally up to the programmer.
What does returning an empty result mean to the caller, conceptually? The idea of these result types is to return either a value or a reason for the lack of value, and an empty state is lacking both the value and the explanation for why the value is missing.
The major refinement of outcome<T>, result<T> and option<T> over
expected
Niall Douglas wrote:
The major refinement of outcome<T>, result<T> and option<T> over expected
is that you **never** get undefined behaviour when using their observers like you do with expected . So when you call .error() for example, you always get well defined semantics and behaviours.
I fully agree with this design decision of yours, by the way. In fact I consider that a defect in expected<>.
you get a C++ exception thrown of type monad_error(no_state).
As a side note, it would be nice from my point of view if you eradicate these last remaining references to 'monad' in the public interface and make that outcome_error (resp. outcome_errc, outcome_category.)
outcome<Foo> found; // default constructs to empty for(auto &i : container) { auto v = something(i); // returns a result<Foo> if(v) // if not errored { found = std::move(v); // auto upconverts break; } } if(!found) { die(); }
OK, let's go with that. Why not construct 'found' initially to contain some error, instead of being empty? You can even define a special errc constant to denote an empty outcome. Sure, this will not throw the exception in your earlier example which accessed v.error() when !v, but is the exception really necessary there? What I'm driving at is that these result types are conceptually (T|E) and the empty state could just be a special case of E. Or, in more general terms, I feel that there's still much extra weight that can be stripped off (not in terms of sizeof, but in terms of the interface.)
2017-05-15 1:35 GMT+02:00 Peter Dimov via Boost
Niall Douglas wrote:
The major refinement of outcome<T>, result<T> and option<T> over
expected
is that you **never** get undefined behaviour when using their observers like you do with expected . So when you call .error() for example, you always get well defined semantics and behaviours. I fully agree with this design decision of yours, by the way. In fact I consider that a defect in expected<>.
you get a C++ exception thrown of type monad_error(no_state).
As a side note, it would be nice from my point of view if you eradicate these last remaining references to 'monad' in the public interface and make that outcome_error (resp. outcome_errc, outcome_category.)
outcome<Foo> found; // default constructs to empty
for(auto &i : container) { auto v = something(i); // returns a result<Foo> if(v) // if not errored { found = std::move(v); // auto upconverts break; } } if(!found) { die(); }
OK, let's go with that. Why not construct 'found' initially to contain some error, instead of being empty? You can even define a special errc constant to denote an empty outcome.
Sure, this will not throw the exception in your earlier example which accessed v.error() when !v, but is the exception really necessary there?
What I'm driving at is that these result types are conceptually (T|E) and the empty state could just be a special case of E.
Or, in more general terms, I feel that there's still much extra weight that can be stripped off (not in terms of sizeof, but in terms of the interface.)
I think there is no good solution here. The expectation of the caller, as
you say, is that the function either returns a `T` or an error (explanation
why if failed to produce a `T`). But on the callee side you often need the
empty state, because the flow would often be like this:
```
vector
Andrzej Krzemienski вроте:
vector
os (20); // no value or error to assign yet for (auto const& e : some_collection) if (cond(e)) os[i].set_value(); else os[i].set_exception();
.set_error presumably. (Incidentally, result<> is documented to have set_exception, which makes no sense to me.)
And in some cases, you cannot immediately rewrite the code in order to avoid default construction. You do not want to return an "empty" object, but you need this value temporarily, end there may be no good value of `E` to use.
The argument that it's better to default-construct to some kind of a valueless state instead of T{} is legitimate, but even if I grant all the premises of that example (which is a stretch), I still don't see why default-constructing to make_error_code( outcome_errc::uninitiaized ) isn't better than special-casing the empty state.
2017-05-15 12:37 GMT+02:00 Peter Dimov via Boost
Andrzej Krzemienski вроте:
vector
os (20); // no value or error to assign yet for (auto const& e : some_collection) if (cond(e)) os[i].set_value(); else os[i].set_exception();
.set_error presumably. (Incidentally, result<> is documented to have set_exception, which makes no sense to me.)
And in some cases, you cannot immediately rewrite the code in order to
avoid default construction. You do not want to return an "empty" object, but you need this value temporarily, end there may be no good value of `E` to use.
The argument that it's better to default-construct to some kind of a valueless state instead of T{} is legitimate, but even if I grant all the premises of that example (which is a stretch), I still don't see why default-constructing to make_error_code( outcome_errc::uninitiaized ) isn't better than special-casing the empty state.
Currently, if you are using library L, the `error_code` describes "disappointments" typical to L's business domain. And an empty `outcome` represents problems related to incorrectly using the Outcome library. IOW, getting and error_code` from library L indicates a robust design that takes into account any run-time situations related to managing resources or irregular user input, whereas an empty state indicates a bug in the code (you may use an empty state temporarily, but you shouldn't return it from the function). I think I am now saying something different than Niall, but that would be my conceptual model for this empty state. Also, to construct some object, it takes time. error_code is just two words, but why waste time on setting them if you will be overriding them in the course of the function call. And, in the current state you can perform a loss-less conversion (or "upgrade") form `option<T>` to `outcome<T>`. Regards, &rzej;
I wished you hadn’t use **NEXT WEEK** shouty tile, it constantly makes me wrongly qualify new posts in this thread as spam.
Andrzej Krzemienski wrote:
Currently, if you are using library L, the `error_code` describes "disappointments" typical to L's business domain. And an empty `outcome` represents problems related to incorrectly using the Outcome library.
Exactly. Returning an uninitialized outcome<> to the caller is incorrect use of library Outcome. Not supposed to happen in your example.
Also, to construct some object, it takes time. error_code is just two words, but why waste time on setting them if you will be overriding them in the course of the function call.
Overly focusing on outcome<>'s runtime performance is odd given that it's usually returned by functions that take millions of cycles to do their job. But either way, it's possible to optimize the default construction to still set a bit and only create the error_code on demand when it's retrieved.
And, in the current state you can perform a loss-less conversion (or "upgrade") form `option<T>` to `outcome<T>`.
option<T> is only present because of the empty state; no empty state, no option<T>. Duplicating opional<T> is questionable, upconverting it to outcome<T> is, too. If you call a function that optionally gives you an object T, and you have to return outcome<T>, you need to provide a reason for the missing T, and the silent upconversion allows you to punt. Error context is lost because only you know why your implementation detail failed to provide the T you asked of it.
Also, to construct some object, it takes time. error_code is just two words, but why waste time on setting them if you will be overriding them in the course of the function call.
Overly focusing on outcome<>'s runtime performance is odd given that it's usually returned by functions that take millions of cycles to do their job.
That may not be the case in some use cases. I've tried very hard to make Outcome's runtime overhead statistically indistinguishable from C integer error code returns (see FAQ for graph).
But either way, it's possible to optimize the default construction to still set a bit and only create the error_code on demand when it's retrieved.
Outcome does do this, but in a different situation. If you try to retrieve a .error() from an excepted Outcome, you get back an error_code of monad_errc::exception_present with category monad_category.
And, in the current state you can perform a loss-less conversion (or "upgrade") form `option<T>` to `outcome<T>`.
option<T> is only present because of the empty state; no empty state, no option<T>. Duplicating opional<T> is questionable, upconverting it to outcome<T> is, too. If you call a function that optionally gives you an object T, and you have to return outcome<T>, you need to provide a reason for the missing T, and the silent upconversion allows you to punt. Error context is lost because only you know why your implementation detail failed to provide the T you asked of it.
I think you might be underestimating the bug detecting value of an
outcome<T> having an unhandled empty state. It traps failure to handle
option<T> correctly very nicely.
Also, don't forget option<T> is the only one from result<T> and
outcome<T> which is usable in constexpr programming. One therefore ends
up using option<T> or expected
.set_error presumably. (Incidentally, result<> is documented to have set_exception, which makes no sense to me.)
Defect logged to https://github.com/ned14/boost.outcome/issues/18. Niall -- ned Productions Limited Consulting http://www.nedproductions.biz/ http://ie.linkedin.com/in/nialldouglas/
I think Niall's choice is a practical one: if you must compromise the interface anyway on one or the other side of the function, at least optimize for performance: implementing the "empty" state comes for free.
It's definitely the case that internally you always need to have an empty state unless you are using Anthony's Williams' double buffer design to enable variants to never ever be empty. But Outcome's API choice was based from the beginning on choosing a ternary logic for the public API. I personally think it's a great fit which is why I chose it, but I appreciate the majority thinks these sorts of class ought to have binary state. Hence Expected's design. As I mentioned to Peter, if you really hate the ternary logic, just don't use the other state. Then you get boolean logic. Niall -- ned Productions Limited Consulting http://www.nedproductions.biz/ http://ie.linkedin.com/in/nialldouglas/
The major refinement of outcome<T>, result<T> and option<T> over expected
is that you **never** get undefined behaviour when using their observers like you do with expected . So when you call .error() for example, you always get well defined semantics and behaviours. I fully agree with this design decision of yours, by the way. In fact I consider that a defect in expected<>.
It's the fault of std::optional<T>. In my opinion it never should have
had silent reinterpret_cast built into its API like that. The cost of
the state check is unmeasurable on any recent CPU, it's not worth not
having.
I've been a strong advocate that the same mistake not be repeated with
expected
you get a C++ exception thrown of type monad_error(no_state).
As a side note, it would be nice from my point of view if you eradicate these last remaining references to 'monad' in the public interface and make that outcome_error (resp. outcome_errc, outcome_category.)
A lot of people grumble at having "monad" in the naming. I don't personally see the problem, it's just a name, and "basic_monad" is exactly what is it: a building block for a monad. I'll tell you what though, if two more people strongly feel that "basic_monad" and "monad_error" need to be renamed, I will do it. Please people do suggest a better alternative name though.
outcome<Foo> found; // default constructs to empty for(auto &i : container) { auto v = something(i); // returns a result<Foo> if(v) // if not errored { found = std::move(v); // auto upconverts break; } } if(!found) { die(); }
OK, let's go with that. Why not construct 'found' initially to contain some error, instead of being empty? You can even define a special errc constant to denote an empty outcome.
You can ask it to be some error too: outcome<Foo> found(make_errored_outcome<Foo>(std::errc::whatever)); If you didn't like the lack of explicit initialisation, you can also do: outcome<Foo> found(empty); // constexpr empty_t empty; let's you tag But the point being made (badly) in the example above was that you can use the empty state to indicate lack of find, and any errored state for errors during find etc. So, let me rewrite the above now it's morning and I am not babbling incoherently: outcome<Foo> found(empty); for(auto &i : container) { auto v = something(i); // returns a result<Foo> if(!v.empty()) // if not empty { found = std::move(v); // auto upconverts preserving error or Foo break; } } if(found.has_error()) { die(found.error()); } if(found) { Do something with found.value() ... } The above is much better. Please disregard what I wrote last night.
What I'm driving at is that these result types are conceptually (T|E) and the empty state could just be a special case of E.
I did consider that design, but I rejected it. outcome<T>, result<T> and option<T> all require E to be some form of error_code_extended, so the user can override E, but not significantly change what contract it promises. You might think then there is an ideal scope for !E to mean empty, but that it turns out is a bad idea. A null error code has the convention of meaning "no error occurred". It does not mean "empty". You could define a special error code category to mean "empty", but now you break all other code using error_code because there is no error_condition which represents "empty". Finally, if you did have some special E value to mean empty, you would have to write special checks for it in Outcome in order to give it the stronger abort type semantics it has - if you don't have it being given alternative semantics, then there is no point in having an empty state. If you are writing special checks, then you might as well just have a formal empty state in the first place. This argument can be generalised into the argument in favour of ternary logics over binary logics. Sure, 90% of the time binary logics are sufficient, but binary is a subset of ternary. And *you don't have to use* the "other" state in a ternary logic just because it's there. Just don't use it, so long as its implementation signals its unintentional usage with a very loud bang, it's a safe design. (There is an argument that Outcome's "very loud bang" isn't loud enough. I'd be pleased to hear feedback on that)
Or, in more general terms, I feel that there's still much extra weight that can be stripped off (not in terms of sizeof, but in terms of the interface.)
As you have surely noticed, I have (intentionally) provided many undisambiguated ways of using Outcome, pushing the problem of which specific combination to use onto the end user. That looks sloppy I am sure. None of my other libraries do that either, they provide one or two "clean" ways of doing any given thing, Outcome is the only library I've done where I give the end user enormous scope to choose their particular style and idioms and conventions and I give no guidance as to which to use (I use different conventions in AFIO v2 and KernelTest myself personally). But it was not done without reason. During my ACCU talk, about half the audience really want implicit conversion for T and E in Expected so you can skip typing make_xxx() all the time. The other half want always explicit instantiation so that it is impossible to create an Expected *without* using make_xxx() (BTW, Toronto will be discussing enforcing this for std::expected i.e. to remove all implicit conversion) The same goes for C++ monadic programming frameworks. There are many diametrically opposed opinions, and no clear evidence which is right and which is wrong. Outcome's monadic API (disabled in this presentation) deliberately sits on the fence and provides just enough hooks to be used by any external monadic programming framework. It doesn't implement a full monadic programming DSEL. I intentionally don't choose sides nor favourites. That upsets almost everyone, but my personal opinion here is that there is no one right answer, sorry. Similarly I intentionally sit on the fence with all the rest of Outcome: it is up to the end user to choose the idiomatic style they prefer. That's why the public API looks "heavy", but really it's just many syntax ways of doing exactly the same thing. The core is extremely simple to keep compiler optimisers happy and generating least possible runtime overhead or code bloat which it very successfully achieves. Ultimately what I would ask is instead of "is this API too fussy?" is rather "is this API unsafe?" or "is this API self contradictory?". I am very interested in feedback on the latter two. Niall -- ned Productions Limited Consulting http://www.nedproductions.biz/ http://ie.linkedin.com/in/nialldouglas/
Niall Douglas wrote:
I'll tell you what though, if two more people strongly feel that "basic_monad" and "monad_error" need to be renamed, I will do it. Please people do suggest a better alternative name though.
basic_monad is an implementation detail, but monad_error is part of the interface.
Similarly I intentionally sit on the fence with all the rest of Outcome: it is up to the end user to choose the idiomatic style they prefer. That's why the public API looks "heavy", but really it's just many syntax ways of doing exactly the same thing.
The problem with this approach, apart from not sitting well with me personally, is that once the library enters wide use, you can no longer take any of these alternative APIs away without breaking code. It's better, in my opinion, to provide a minimal interface first, then add bells and whistles on an as-needed basis. I look at result<>'s reference documentation and all I can think of is that 2/3 of it isn't needed and 2/3 of that even consists of different spellings of the same thing. I'd even remove value_or, there's nothing wrong with r? r.value(): def.
During my ACCU talk, about half the audience really want implicit conversion for T and E in Expected so you can skip typing make_xxx() all the time. The other half want always explicit instantiation so that it is impossible to create an Expected *without* using make_xxx() (BTW, Toronto will be discussing enforcing this for std::expected i.e. to remove all implicit conversion)
A legitimate fork in the road. What I'd do is enable conversion when unambiguous, otherwise require make_expected<T> and make_unpexpected<E>. But that's not what we're talking about here, because make_expected and make_unexpected will be present regardless of the decision to enable implicit conversion or not.
Another defect in Expected in my opinion is having value return semantics for .value_or(). You'll note Outcome's Expected has reference return semantics for .value_or() which is a deviation.
I don't care for the pervasive &/&&/const&/const&& duplication (fourplication) very much myself, would return by value everywhere, but that's a matter of taste. Follows from the philosophy to provide the simple thing first, then complicate if need arises.
Similarly I intentionally sit on the fence with all the rest of Outcome: it is up to the end user to choose the idiomatic style they prefer. That's why the public API looks "heavy", but really it's just many syntax ways of doing exactly the same thing.
The problem with this approach, apart from not sitting well with me personally,
I do want to emphasise that I agree with you. Outcome is the only C++ library I've ever written which was multi-paradigm. Normally I decide on one "right" way to do things, and enforce that on end users. You may remember that from the AFIO v1 review where everyone didn't like my choice of single vision, hence universal rejection bar one. But in the end, I am overwhelmed by evidence that a single "right" way is the wrong design here in this one very unusual situation. In my own libraries which use Outcome I am using different conventions and use patterns. I tried to summarise those with catchy names for the design patterns in the Outcome tutorial, but to quote from https://ned14.github.io/boost.outcome/md_doc_md_07-faq.html#examples_of_use: "The main reason I designed and wrote Outcome was to implement a universal error handling framework which could express in the minimum possible overhead, and without losing information, the many C++ error handling design patterns possible, and even more importantly that individual libraries could use the design pattern which best suited them whilst seamlessly interoperating with other libraries using different error handling designs. To go into a bit more detail: * Proposed Boost.AFIO v2 is a very low level very thin file i/o and filesystem library which sits just above the raw kernel syscalls. Throwing exceptions in such a library is overkill, so AFIO v2 uses the "sea of noexcept" design pattern both in its public API and in its internal implementation (i.e. it doesn't use C++ exceptions at all). * Proposed Boost.KernelTest is a kernel based testing infrastructure which uses Outcomes as the storage for each kernel permutation run in its permutation tables of preconditions, postconditions, parameters and outcomes. KernelTest itself is written using the "exceptions are exceptional" design pattern where expected errors are returned via outcomes but unexpected errors which abort the test use thrown exceptions which are collected into outcome<T>'s. AFIO v2's test suite is written using KernelTest. * Planned Boost.BLOBStore will be a versioned, ACID transactional key to BLOB store written using AFIO v2 which will use both the "sea of noexcept" and the "exceptions are exceptional" design patterns together. This allows user supplied callbacks to throw exceptions which aborts the current transaction and for those exceptions to be propagated, if desired, out of BLOBStore whilst the internal implementation of BLOBStore and indeed its public API is all noexcept and never throws exceptions (writing correct filesystem code is hard enough without dealing with unexpected control flow reversal). BLOBStore will also use KernelTest for its test suite." I know you've read the history page Peter, so you know Outcome has already had its API trimmed by half. These past 18 months I've been removing stuff, paring down to the minimum. I don't claim I'm done removing stuff yet, I agree there is a little more to go, but it's getting harder to decide on what isn't really necessary any more.
is that once the library enters wide use, you can no longer take any of these alternative APIs away without breaking code.
I absolutely agree with this assessment. Which is why Outcome's ABI is versioned, and can be iterated with breaking changes if needs be. Right now the ABI is permuted per git commit with the commit SHA as Outcome is unstable. Once it's been firmed up through people using it more, I'll declare a stable ABI which will be written in stone and tested with abi-compliance-checker per commit. But thanks to the ABI versioning, I can also make breaking changes without breaking code if it turns out I made a terrible mistake in semantics.
It's better, in my opinion, to provide a minimal interface first, then add bells and whistles on an as-needed basis. I look at result<>'s reference documentation and all I can think of is that 2/3 of it isn't needed and 2/3 of that even consists of different spellings of the same thing.
Are you referring to .get() and .value()? I use a convention of .value() in my own code to indicate when I am retrieving the value, and .get() to indicate I am throwing away the fetched value but I do want any default actions to occur e.g. if not valued, throw an exception. So .get() means "fetch and throw any error state if present". If people like this convention, I can make it formal by tagging .value()'s return with [[nodiscard]] and have .get() return void. If people dislike this convention, .get() can be removed. I really wasn't sure what people might prefer. [[nodiscard]] is so new it's hard to decide on if we are using it overkill or not. I look forward to any feedback.
I'd even remove value_or, there's nothing wrong with r? r.value(): def.
Both optional and expected have .value_or(). I've also found the ternary operator a poor substitute for .value_or() in practice because both sides need to be the same type else the compiler complains. .value_or() coerces the type properly. Less surprise and less typing.
During my ACCU talk, about half the audience really want implicit conversion for T and E in Expected so you can skip typing make_xxx() all the time. The other half want always explicit instantiation so that it is impossible to create an Expected *without* using make_xxx() (BTW, Toronto will be discussing enforcing this for std::expected i.e. to remove all implicit conversion)
A legitimate fork in the road. What I'd do is enable conversion when unambiguous, otherwise require make_expected<T> and make_unpexpected<E>.
I completely agree for the Expected proposal. But Vicente doesn't like that idea. Regarding the community disagreement being a legitimate fork in the road, I used to think that. But as I hopefully demonstrated above, as I design more libraries using Outcome I find myself using multiple paradigms, and I think I am right to have done that for each library in question. I claim therefore that this is not a fork in the road, but rather parallel roads all starting from the same place and ending in the same place. One therefore needs an error handling system which easily lets you "cross lanes" between those parallel roads simply and without losing original information. You thus get Outcome.
Another defect in Expected in my opinion is having value return semantics for .value_or(). You'll note Outcome's Expected has reference return semantics for .value_or() which is a deviation.
I don't care for the pervasive &/&&/const&/const&& duplication (fourplication) very much myself, would return by value everywhere, but that's a matter of taste. Follows from the philosophy to provide the simple thing first, then complicate if need arises.
Alas Expected and Outcome permit usage with types with no default
constructor, no copy nor move. If this were not the case, I'd agree with
returning by value everywhere. But if you permit types limited like
that, returning by value anywhere in your API ought to be ruled out as
causing needless surprise and consternation to end users when an API
suddenly stops working just because the type used has changed.
BTW, is everyone aware that expected
Niall Douglas wrote:
I know you've read the history page Peter, so you know Outcome has already had its API trimmed by half. These past 18 months I've been removing stuff, paring down to the minimum. I don't claim I'm done removing stuff yet, I agree there is a little more to go, but it's getting harder to decide on what isn't really necessary any more.
I know that, which is why I'd prefer the removal to proceed to its logical conclusion. :-)
Are you referring to .get() and .value()?
Not just that; is_ready, get_or, set_*, op->, op*, get_error, get_error_or.
I'd even remove value_or, there's nothing wrong with r? r.value(): def.
Both optional and expected have .value_or().
Yeah, they are also wrong in having it. You/we don't need to copy std::optional. It's not a good example to follow.
I've also found the ternary operator a poor substitute for .value_or() in practice because both sides need to be the same type else the compiler complains.
That's more of a feature. If you're passing something else this may well indicate a mistake.
Alas Expected and Outcome permit usage with types with no default constructor, no copy nor move.
This would rely on guaranteed return value elision and return { in_place, args... }, I suppose? If these classes at some point enter the standard, the committee will inevitably adorn them with all this beauty, but until then, no need to prematurely do that ourselves.
BTW, is everyone aware that expected
is legal?
I wasn't aware that E could be void. Which reminds me to ask you, do you really drop the value on the floor when converting from <T> to <void>? This seems like something to avoid. The value is probably important.
I know that, which is why I'd prefer the removal to proceed to its logical conclusion. :-)
Are you referring to .get() and .value()?
Not just that; is_ready, get_or, set_*, op->, op*, get_error, get_error_or.
I'll accept the .is_ready(), .get_or(), get_error_or(). Logged to https://github.com/ned14/boost.outcome/issues/19 The op-> and op* are present in expected and optional. I dislike their reinterpret_cast semantics as much as you do, but if I removed them end users would complain. The set_*() functions save considerable typing and boilerplate. The .get_error() and .get_exception() I may do the same thing as I have proposed for .get() whereby they mean "do the same default actions as the non-get edition, but don't return reference to state". I'd love to know reviewers' opinion on that, including whether to mark the non-get function with [[nodiscard]].
I'd even remove value_or, there's nothing wrong with r? r.value(): def.
Both optional and expected have .value_or().
Yeah, they are also wrong in having it. You/we don't need to copy std::optional. It's not a good example to follow.
I think such convenience member functions harmless, apart possibly from increasing cognitive load. And users don't have to use them if they don't want to. But again, I'd be interested to see how many others think .value_or() needs to die, and similarly for .error_or() and .exception_or().
Alas Expected and Outcome permit usage with types with no default constructor, no copy nor move.
This would rely on guaranteed return value elision and return { in_place, args... }, I suppose?
We don't need to depend on guaranteed return value elision. There is an emplacing constructor, an initializer_list constructor, and the observers return references. So one does not need a default constructor, nor copy nor move.
If these classes at some point enter the standard, the committee will inevitably adorn them with all this beauty, but until then, no need to prematurely do that ourselves.
I have no intent for any of the Outcome refinements to enter the standard. Expected is plenty enough for the standard as a primitive which can be built upon and extended. I deliberately have made sure that a result<T> is commensurate to an expected<T> so AFIO v2, if it ever goes to be standardised, can be based around expected<T>. I do intend to submit Outcome for SG14's collection of low latency libraries in the non-standards-proposal category.
BTW, is everyone aware that expected
is legal? I wasn't aware that E could be void. Which reminds me to ask you, do you really drop the value on the floor when converting from <T> to <void>? This seems like something to avoid. The value is probably important.
You cannot convert, even explicitly, from any Outcome-y <T> transport to a <void> transport. It's disallowed because it causes data loss. You must either manually extract and repack the E into a new <void> transport, or there is a free function as_void() which will do it for you. The latter should produce tighter code and compile faster, it uses special magic type sugar to bypass implementation as BOOST_OUTCOME_TRY(v, expr) always returns a void converted edition of expression, and in real world usage you will using a lot of BOOST_OUTCOME_TRY(v, expr) so it needs to be light on compile times. BTW I leave for a business trip other side of the country tomorrow and Wednesday. Expect no replies here until Thursday. Niall -- ned Productions Limited Consulting http://www.nedproductions.biz/ http://ie.linkedin.com/in/nialldouglas/
Niall Douglas wrote:
We don't need to depend on guaranteed return value elision. There is an emplacing constructor, an initializer_list constructor, and the observers return references. So one does not need a default constructor, nor copy nor move.
How are you going to return it from a function then? That's what these types are, function return values.
We don't need to depend on guaranteed return value elision. There is an emplacing constructor, an initializer_list constructor, and the observers return references. So one does not need a default constructor, nor copy nor move.
How are you going to return it from a function then? That's what these types are, function return values.
That's an interesting observation.
Vicente, when he reviewed Outcome's docs, was pretty appalled at how
error handling focused they are. His view is that Expected is an EITHER
monad where E is exactly like T, and he was not happy that Outcome
presents Expected as if it is solely for use for returning stuff from
functions.
And, he is right. Expected has a raft of other uses especially in
functional metaprogramming, and Outcome and its docs does not do justice
to any of those other uses.
But my counter argument is that 70-80% of people out there currently
don't care about those other uses. I know this from having to completely
rewrite the Outcome docs three times. They currently only want to know
about error handling. So I made the docs all about that only.
A non-copyable nor moveable expected
Niall Douglas wrote:
Vicente, when he reviewed Outcome's docs, was pretty appalled at how error handling focused they are. His view is that Expected is an EITHER monad where E is exactly like T, and he was not happy that Outcome presents Expected as if it is solely for use for returning stuff from functions.
And, he is right.
No, he isn't. The whole point of expected<> is that T is not like E. That's
why T is _expected_, and E is _unexpected_. It's in the name.
When T is like E, you use variant
Vicente, when he reviewed Outcome's docs, was pretty appalled at how error handling focused they are. His view is that Expected is an EITHER monad where E is exactly like T, and he was not happy that Outcome presents Expected as if it is solely for use for returning stuff from functions.
And, he is right.
No, he isn't. The whole point of expected<> is that T is not like E. That's why T is _expected_, and E is _unexpected_. It's in the name.
When T is like E, you use variant
. Or variant . My own idea of expected
takes one T and an arbitrary amount of Es, and while all Es are alike, T isn't. And this aside, the general idea is to provide alternative to exceptions, which kind of implies returning stuff from functions, does it not?
As you might guess, Vicente would say that Expected *can* be used for returning stuff from functions, but also a long list of other stuff. But I won't put words in his mouth. He is subscribed here too after all, he can answer himself if he chooses to. Outcome doesn't get a ton of choice with its Expected implementation. Either it implements the Expected proposal for standardisation, or it doesn't. I don't want the Outcome refinements to veer too strongly away from LEWG Expected, they ought to be similar, except where it's necessary to be different, in order to make it cognitively easy for end users. Even where I personally think Expected is making a mistake. As much as I've kicked up a fuss in some places by intentionally deviating from LEWG Expected to force some attention at where I think there are problems, after this review I'll go ahead and conform as tightly as I reasonably can and I'll track the LEWG proposal over time. A deviating implementation muddies the waters, is confusing for end users, and helps no one. BTW Outcome's Expected now lives in an experimental namespace on develop branch. I'll post a full set of changes already logged in this pre-review when the review officially starts on Friday so people don't waste time repeating things already logged by others. Niall -- ned Productions Limited Consulting http://www.nedproductions.biz/ http://ie.linkedin.com/in/nialldouglas/
Niall Douglas wrote:
As much as I've kicked up a fuss in some places by intentionally deviating from LEWG Expected to force some attention at where I think there are problems, after this review I'll go ahead and conform as tightly as I reasonably can and I'll track the LEWG proposal over time. A deviating implementation muddies the waters, is confusing for end users, and helps no one.
A conforming expected<> is fine, but result<> and outcome<> do not have to provide the same interface. I'd rather see them address the function return use case in a clean, efficient, clutter-free way, rather than trying to be something else. (I'd also rather see expected<> address that same use case instead of trying to be usable as a long list of something else, but that's, as you would say, out of scope for this review.)
Le 16/05/2017 à 00:48, Niall Douglas via Boost a écrit :
Vicente, when he reviewed Outcome's docs, was pretty appalled at how error handling focused they are. His view is that Expected is an EITHER monad where E is exactly like T, and he was not happy that Outcome presents Expected as if it is solely for use for returning stuff from functions.
And, he is right. No, he isn't. The whole point of expected<> is that T is not like E. That's why T is _expected_, and E is _unexpected_. It's in the name.
When T is like E, you use variant
. Or variant . My own idea of expected
takes one T and an arbitrary amount of Es, and while all Es are alike, T isn't. And this aside, the general idea is to provide alternative to exceptions, which kind of implies returning stuff from functions, does it not? As you might guess, Vicente would say that Expected *can* be used for returning stuff from functions, but also a long list of other stuff.
But I won't put words in his mouth. He is subscribed here too after all, he can answer himself if he chooses to. Thanks Niall. I can speak for myself. Vicente
Le 16/05/2017 à 00:22, Peter Dimov via Boost a écrit :
Niall Douglas wrote:
Vicente, when he reviewed Outcome's docs, was pretty appalled at how error handling focused they are. His view is that Expected is an EITHER monad where E is exactly like T, and he was not happy that Outcome presents Expected as if it is solely for use for returning stuff from functions. No I have never said that E is like T.
And, he is right.
No, he isn't. The whole point of expected<> is that T is not like E. That's why T is _expected_, and E is _unexpected_. It's in the name.
When T is like E, you use variant
. Or variant . My own idea of expected
takes one T and an arbitrary amount of Es, and while all Es are alike, T isn't. And this aside, the general idea is to provide alternative to exceptions, which kind of implies returning stuff from functions, does it not? I agree completly.
expected
On 16/05/2017 03:48, Niall Douglas wrote:
Are you referring to .get() and .value()?
I use a convention of .value() in my own code to indicate when I am retrieving the value, and .get() to indicate I am throwing away the fetched value but I do want any default actions to occur e.g. if not valued, throw an exception. So .get() means "fetch and throw any error state if present".
If people like this convention, I can make it formal by tagging .value()'s return with [[nodiscard]] and have .get() return void. If people dislike this convention, .get() can be removed.
I really wasn't sure what people might prefer. [[nodiscard]] is so new it's hard to decide on if we are using it overkill or not. I look forward to any feedback.
Maybe I'm missing some context, but unless T is void, making a void get() method seems very peculiar to me. Logically a get method should actually get something. (void get() where T is void is also weird, but at least its behaviour is familiar to anyone used to futures, and is reasonably orthogonal to the non-void T case, and has some benefits to *some* generic code -- though sadly void is still a second-class citizen in the type system.)
Gavin Lambert wrote:
Maybe I'm missing some context, but unless T is void, making a void get() method seems very peculiar to me. Logically a get method should actually get something.
get() is not a very good name, it's more like ensure(), and the intended use is in the transition from an outcome<>-using layer to a throwing layer: T f() // throws { outcome<X> o1 = f1_impl(); // doesn't throw o1.ensure(); // ensure we have a value outcoume<Y> o2 = f2_impl(); o2.ensure(); return { *o1, *o2 }; } The reference access paradigm implies that we want to keep the values in the outcomes, so there's no need to actually extract them, we can use them in-place. In contrast, the value return paradigm implies that in this scenario we want to extract the values from the outcomes and throw the outcomes away: T f() { X x = f1_impl().value(); Y y = f2_impl().value(); return { x, y }; } No need for a void get() here. Or, if we want to be verbose: T f() { outcome<X> o1 = f1_impl(); X x = std::move(o1).value(); outcome<Y> o2 = f2_impl(); Y y = std::move(o2).value(); return { x, y }; } although in this case o1 and o2 stay in scope and we prefer them not to because we don't want to access them after the values have been extracted. It's more explicit though so to each his own.
On 16/05/2017 11:47, Peter Dimov wrote:
Gavin Lambert wrote:
Maybe I'm missing some context, but unless T is void, making a void get() method seems very peculiar to me. Logically a get method should actually get something.
get() is not a very good name, it's more like ensure(), and the intended use is in the transition from an outcome<>-using layer to a throwing layer:
T f() // throws { outcome<X> o1 = f1_impl(); // doesn't throw o1.ensure(); // ensure we have a value
outcoume<Y> o2 = f2_impl(); o2.ensure();
return { *o1, *o2 }; }
The reference access paradigm implies that we want to keep the values in the outcomes, so there's no need to actually extract them, we can use them in-place.
In contrast, the value return paradigm implies that in this scenario we want to extract the values from the outcomes and throw the outcomes away:
T f() { X x = f1_impl().value(); Y y = f2_impl().value();
return { x, y }; }
Surely this must also throw if f1_impl() can produce an error outcome and thus might not contain an X? I don't see the distinction. (Use of outcome<X> above implies that X is not itself an error-capable type.)
Gavin Lambert wrote:
Surely this must also throw if f1_impl() can produce an error outcome and thus might not contain an X?
Yes of course, the examples are deliberately identical semantically, one uses "void get()" and the other uses "T value()". I prefer the second.
On 16/05/2017 13:50, Peter Dimov wrote:
Surely this must also throw if f1_impl() can produce an error outcome and thus might not contain an X?
Yes of course, the examples are deliberately identical semantically, one uses "void get()" and the other uses "T value()". I prefer the second.
Future uses "T get()". This is established standard. Smart pointers use "T* get()", which is equivalent to the above. Also standard. Boost Optional uses "T& get()" (although also provides value(), presumably for standard compatibility). Std Optional uses "T value()". This is either still experimental or approved for C++17; I'm not entirely sure how far along that is. The different interfaces of the above seem unfortunate (and Std Optional seems incorrect in this regard to me). So why not "T get()"? That's much more consistent with other types.
Gavin Lambert wrote:
Future uses "T get()". This is established standard. Smart pointers use "T* get()", which is equivalent to the above. Also standard. Boost Optional uses "T& get()" (although also provides value(), presumably for standard compatibility).
Std Optional uses "T value()". This is either still experimental or approved for C++17; I'm not entirely sure how far along that is.
It's in C++17.
The different interfaces of the above seem unfortunate (and Std Optional seems incorrect in this regard to me).
So why not "T get()"? That's much more consistent with other types.
.get() is when you only have one thing to get; it's for one-element
containers, where there's no ambiguity as to what is being got.
expected
On 17/05/2017 11:34, Peter Dimov wrote:
So why not "T get()"? That's much more consistent with other types.
.get() is when you only have one thing to get; it's for one-element containers, where there's no ambiguity as to what is being got.
expected
and friends have two; the value is .value() and the error is .error(), with their matching queries has_value() and has_error().
But by definition these are not treated equally. The value is the expected thing (thus get() makes sense to retrieve it) and the error or exception are the unexpected things (thus some other method). I don't see any ambiguity there.
It's not that get() is unacceptably wrong, but the value/has_value/error/has_error convention is legitimate and consistent.
Having "T value()" as the only API is fine, but seems a bit inconsistent as that's not the common verb used by other types (in fact it's not even a verb). (std::get is another argument in favour of this.) Having "T value()" and "T get()" is ok (it's sort of like Boost Optional that way) but it's a bit redundant and it would just make people second-guess which one they should use, or if they're different in some way. Having "T value()" and "void get()" is just bananas. Though for Niall's proposed usage, perhaps "void ensure()" would make sense. Regarding [[nodiscard]], I don't think either of these methods should have that. Instead it should be the method that returns the expected<> or outcome<> that declares its return value to be [[nodiscard]] (if it wishes). Though I recall another proposed library using some trick (possibly involving rvalue-methods?) to ensure a compiler error if the return value was uninspected without explicitly using [[nodiscard]].
On 17/05/2017 00:25, Gavin Lambert via Boost wrote:
On 16/05/2017 13:50, Peter Dimov wrote:
Surely this must also throw if f1_impl() can produce an error outcome and thus might not contain an X?
Yes of course, the examples are deliberately identical semantically, one uses "void get()" and the other uses "T value()". I prefer the second.
Future uses "T get()". This is established standard. Smart pointers use "T* get()", which is equivalent to the above. Also standard. Boost Optional uses "T& get()" (although also provides value(), presumably for standard compatibility).
Std Optional uses "T value()". This is either still experimental or approved for C++17; I'm not entirely sure how far along that is.
The different interfaces of the above seem unfortunate (and Std Optional seems incorrect in this regard to me).
So why not "T get()"? That's much more consistent with other types.
Outcome provided only get() until last January when I added an Expected implementation. I have always felt that "get()" implied a potentially blocking operation somehow. Something about the word. My main rationale for choosing get() over value() was simple: the former is two characters less to type. Other than that, I feel unbothered as to which is better, and as you notice in the submitted library I provided both and pushed which to choose onto end users. I'm getting the feeling that that isn't considered a good a idea by reviewers so far, and you all want me to choose one, and not use .get() for an operation which doesn't return a value. .ensure() has been suggested for this. So are you happy or unhappy with this plan: 1. .get(), .get_error() and .get_exception() go away. 2. .ensure(empty|value|error|exception) will perform the default actions of calling the observer function for that state, but not return that state. Note that empty, value, error and exception are already constexpr variables in the outcome namespace used for tagging. Thoughts? Niall -- ned Productions Limited Consulting http://www.nedproductions.biz/ http://ie.linkedin.com/in/nialldouglas/
2017-05-18 17:16 GMT+02:00 Niall Douglas via Boost
On 17/05/2017 00:25, Gavin Lambert via Boost wrote:
On 16/05/2017 13:50, Peter Dimov wrote:
Surely this must also throw if f1_impl() can produce an error outcome and thus might not contain an X?
Yes of course, the examples are deliberately identical semantically, one uses "void get()" and the other uses "T value()". I prefer the second.
Future uses "T get()". This is established standard. Smart pointers use "T* get()", which is equivalent to the above. Also standard. Boost Optional uses "T& get()" (although also provides value(), presumably for standard compatibility).
Std Optional uses "T value()". This is either still experimental or approved for C++17; I'm not entirely sure how far along that is.
The different interfaces of the above seem unfortunate (and Std Optional seems incorrect in this regard to me).
So why not "T get()"? That's much more consistent with other types.
Outcome provided only get() until last January when I added an Expected implementation.
I have always felt that "get()" implied a potentially blocking operation somehow. Something about the word.
My main rationale for choosing get() over value() was simple: the former is two characters less to type. Other than that, I feel unbothered as to which is better, and as you notice in the submitted library I provided both and pushed which to choose onto end users.
I'm getting the feeling that that isn't considered a good a idea by reviewers so far, and you all want me to choose one, and not use .get() for an operation which doesn't return a value. .ensure() has been suggested for this.
So are you happy or unhappy with this plan:
1. .get(), .get_error() and .get_exception() go away.
Yes.
2. .ensure(empty|value|error|exception) will perform the default actions of calling the observer function for that state, but not return that state.
Weak yes. - I am not sure why you need this fancy syntax.
Note that empty, value, error and exception are already constexpr variables in the outcome namespace used for tagging.
Thoughts?
Also, get rid of `is_ready()` if it does the same thing as `empty()`. And maybe `is_empty()` would be less ambiguous: word "empty" is also a verb in english. Regards, &rzej;
2. .ensure(empty|value|error|exception) will perform the default actions of calling the observer function for that state, but not return that state.
Weak yes. - I am not sure why you need this fancy syntax.
The constexpr variables are already there. So basically do we prefer: 1. .ensure_empty(), .ensure_value(), .ensure_error() and .ensure_exception() 2. Or .ensure(empty), .ensure(value), .ensure(error) and .ensure(exception) I figure the latter looked nicer. It's same difference to the compiler, simple overload matching is constant time.
Also, get rid of `is_ready()` if it does the same thing as `empty()`. And maybe `is_empty()` would be less ambiguous: word "empty" is also a verb in english.
Logged some time ago to https://github.com/ned14/boost.outcome/issues/19. I'll post a full changelog of all things already changed and a list of already agreed changes to come tomorrow when the review begins. I won't be changing the edition posted for review however, current develop branch has seen a lot of not well tested change this past week thanks to all the feedback so far. Niall -- ned Productions Limited Consulting http://www.nedproductions.biz/ http://ie.linkedin.com/in/nialldouglas/
On 19/05/2017 11:11, Niall Douglas wrote:
2. .ensure(empty|value|error|exception) will perform the default actions of calling the observer function for that state, but not return that state.
Weak yes. - I am not sure why you need this fancy syntax.
The constexpr variables are already there. So basically do we prefer:
1. .ensure_empty(), .ensure_value(), .ensure_error() and .ensure_exception()
2. Or .ensure(empty), .ensure(value), .ensure(error) and .ensure(exception)
I figure the latter looked nicer. It's same difference to the compiler, simple overload matching is constant time.
But why have them at all, rather than just calling value(), error(), etc and discarding the return value? They're also constexpr. Also, what code patterns would lead to wanting to do that frequently anyway? Given that the whole point is to transport a value or an error, it seems odd to have code paths that want to throw if a value or error was not produced but then not actually care what the "right" thing was.
The constexpr variables are already there. So basically do we prefer:
1. .ensure_empty(), .ensure_value(), .ensure_error() and .ensure_exception()
2. Or .ensure(empty), .ensure(value), .ensure(error) and .ensure(exception)
I figure the latter looked nicer. It's same difference to the compiler, simple overload matching is constant time.
But why have them at all, rather than just calling value(), error(), etc and discarding the return value? They're also constexpr.
Code correctness auditing. You can prefix o.value() with a (void) of course, but I think the potential [[nodiscard]] on the reference returning observers might be a gain (note that the end user applying [[nodiscard]] to the types used won't work because of the reference return).
Also, what code patterns would lead to wanting to do that frequently anyway? Given that the whole point is to transport a value or an error, it seems odd to have code paths that want to throw if a value or error was not produced but then not actually care what the "right" thing was.
The default actions are extremely useful at an outcome-to-c++-exception boundary where low level Outcome returning code is used by higher level code exclusively using C++ exceptions. The default actions save considerable boilerplate checking state by hand etc. Anyone who has used AFIO v2 will know exactly what I mean. Niall -- ned Productions Limited Consulting http://www.nedproductions.biz/ http://ie.linkedin.com/in/nialldouglas/
2017-05-19 1:11 GMT+02:00 Niall Douglas via Boost
2. .ensure(empty|value|error|exception) will perform the default actions of calling the observer function for that state, but not return that state.
Weak yes. - I am not sure why you need this fancy syntax.
The constexpr variables are already there. So basically do we prefer:
1. .ensure_empty(), .ensure_value(), .ensure_error() and .ensure_exception()
2. Or .ensure(empty), .ensure(value), .ensure(error) and .ensure(exception)
I figure the latter looked nicer. It's same difference to the compiler, simple overload matching is constant time.
At the end of the day it is just syntax, so your choice is a as good as any other. But if you use those "constants", one might ask why are you not using them consistently. You could also get rid of `value()`, `error()`, `exception()` and instead provide `get(value)`, `get(error)`, `get(exception)`. I am not proposing it, though. Just one note. If I use namspace prefixes, the notation with "constants" becomes longer: `o.ensure_empty()` becomes `o.ensure(boost::outcome::empty)` One could respond to this "just import anything from namespace `boost::outcome` into the scope", but that is imposing on me a certain style of programming, which I not necessarily want to adapt. Regards, &rzej;
The constexpr variables are already there. So basically do we prefer:
1. .ensure_empty(), .ensure_value(), .ensure_error() and .ensure_exception()
2. Or .ensure(empty), .ensure(value), .ensure(error) and .ensure(exception)
I figure the latter looked nicer. It's same difference to the compiler, simple overload matching is constant time.
Just one note. If I use namspace prefixes, the notation with "constants" becomes longer:
`o.ensure_empty()` becomes `o.ensure(boost::outcome::empty)`
One could respond to this "just import anything from namespace `boost::outcome` into the scope", but that is imposing on me a certain style of programming, which I not necessarily want to adapt.
Yes that's a good point. Also, after a few nights of sleeping on it, I'm not keen on .ensure_XXX(). Would people be okay with: * o.check() <= (void) o.value() * o.check_error() <= (void) o.error() * o.check_exception() <= (void) o.exception() Niall -- ned Productions Limited Consulting http://www.nedproductions.biz/ http://ie.linkedin.com/in/nialldouglas/
On Fri, May 19, 2017 at 6:08 AM, Niall Douglas via Boost
...
Boost.Outcome fails to build for me. I'm using Visual Studio 2017 with the latest update, tip of the master branch. My steps were as follows; mkdir bin cd bin cmake .. Open outcome.sln, Rebuild All Attached is a log file with the errors. In particular the __has_include preprocessor is a clang extension, and unavailable in MSVC. There seem to be other syntax errors. Thanks
On Fri, May 19, 2017 at 6:08 AM, Niall Douglas via Boost
...
I'm assuming that this code somehow avoids the double-checked lock for static objects with function scope: https://github.com/ned14/boost.outcome/blob/master/include/boost/outcome/v1.... Can you please explain how it achieves this goal on all platforms? I'd also like to understand this declaration: BOOSTLITE_NOINLINE inline const std::error_category &_generic_category() Why is there a no inline macro followed by the inline keyword?
I'm assuming that this code somehow avoids the double-checked lock for static objects with function scope: https://github.com/ned14/boost.outcome/blob/master/include/boost/outcome/v1....
Can you please explain how it achieves this goal on all platforms?
I'd also like to understand this declaration:
BOOSTLITE_NOINLINE inline const std::error_category &_generic_category()
Why is there a no inline macro followed by the inline keyword?
Inline linkage to allow multiple symbols, but never inline. Some STLs (Dinkumware) ensure that the address of an error_category is always unique throughout a process as per the C++ standard requirement. Their code for figuring out a canonical singleton is a thread fence, force emitting code, and some optimisers e.g. MSVC's give up at folding code very quickly when they see a thread fence. This causes sequences of result<T> or expected<T> to not be elided and collapsed by the optimiser as we'd prefer. The hack is the above. We cache the address of the canonical singleton, and the noinline seems to cause the optimiser to disregard the thread fence and thus to not give up quickly. The resulting assembler generated is greatly improved on MSVC, a single result<T> shrinks from ~260 opcodes to less than 5. I left the hack enabled on all platforms as it's benign and future STLs might do something like Dinkumware. I might add that the hack has no statistical effect on performance, but it reduces code bloat. Niall -- ned Productions Limited Consulting http://www.nedproductions.biz/ http://ie.linkedin.com/in/nialldouglas/
On Fri, May 19, 2017 at 7:05 AM, Niall Douglas via Boost
... The hack is the above. We cache the address of the canonical singleton, and the noinline seems to cause the optimiser to disregard the thread fence and thus to not give up quickly. The resulting assembler generated is greatly improved on MSVC, a single result<T> shrinks from ~260 opcodes to less than 5.
As I use error categories extensively in my library, a blog post or article focusing on this technique would be useful (if it doesn't already exist).
The hack is the above. We cache the address of the canonical singleton, and the noinline seems to cause the optimiser to disregard the thread fence and thus to not give up quickly. The resulting assembler generated is greatly improved on MSVC, a single result<T> shrinks from ~260 opcodes to less than 5.
As I use error categories extensively in my library, a blog post or article focusing on this technique would be useful (if it doesn't already exist).
It's a micro optimisation which only affects code bloat on one particular compiler currently.. I didn't, and still don't, think it worth writing up. Niall -- ned Productions Limited Consulting http://www.nedproductions.biz/ http://ie.linkedin.com/in/nialldouglas/
Niall Douglas wrote:
https://github.com/ned14/boost.outcome/blob/master/include/boost/outcome/v1....
The hack is the above. We cache the address of the canonical singleton, ...
Hm. I must be missing something. BOOSTLITE_NOINLINE inline const std::error_category &_generic_category() { const std::error_category &c = stl11::generic_category(); return c; } This doesn't cache anything. It just calls stl11::generic_category() and returns the result.
The resulting assembler generated is greatly improved on MSVC, a single result<T> shrinks from ~260 opcodes to less than 5.
Would be nice if we could see that demonstrated on godbolt.org.
Would be nice if we could see that demonstrated on godbolt.org.
https://godbolt.org/g/QeHRpl std::error_code f() { return std::error_code( 5, std::system_category() ); } -> ___$ReturnUdt$ = 8 ; size = 4 f PROC call std::_Immortalizestd::_System_error_category mov ecx, DWORD PTR ___$ReturnUdt$[esp-4] mov DWORD PTR [ecx+4], eax mov eax, ecx mov DWORD PTR [ecx], 5 ret 0 f ENDP
Although that's VS 2017, and godbolt.org doesn't have 15. :-/ -----Original Message----- From: Peter Dimov via Boost Sent: Friday, May 19, 2017 17:59 To: boost@lists.boost.org Cc: Peter Dimov Subject: Re: [boost] [review] Review of Outcome (starts Fri-19-May)
Would be nice if we could see that demonstrated on godbolt.org.
https://godbolt.org/g/QeHRpl std::error_code f() { return std::error_code( 5, std::system_category() ); } -> ___$ReturnUdt$ = 8 ; size = 4 f PROC call std::_Immortalizestd::_System_error_category mov ecx, DWORD PTR ___$ReturnUdt$[esp-4] mov DWORD PTR [ecx+4], eax mov eax, ecx mov DWORD PTR [ecx], 5 ret 0 f ENDP _______________________________________________ Unsubscribe & other changes: http://lists.boost.org/mailman/listinfo.cgi/boost
VS 2015, -Ox -Ob2: ; COMDAT ?f@@YA?AVerror_code@std@@XZ _TEXT SEGMENT ___$ReturnUdt$ = 8 ; size = 4 ?f@@YA?AVerror_code@std@@XZ PROC ; f, COMDAT push ebp mov ebp, esp call ??$_Immortalize@V_System_error_category@std@@@std@@YAAAV_System_error_category@0@XZ ; std::_Immortalizestd::_System_error_category mov ecx, DWORD PTR ___$ReturnUdt$[ebp] mov DWORD PTR [ecx+4], eax mov eax, ecx mov DWORD PTR [ecx], 5 pop ebp ret 0 That's update 3 though, things may have been different in u0..u2. -----Original Message----- From: Peter Dimov via Boost Sent: Friday, May 19, 2017 18:01 To: boost@lists.boost.org Cc: Peter Dimov Subject: Re: [boost] [review] Review of Outcome (starts Fri-19-May) Although that's VS 2017, and godbolt.org doesn't have 15. :-/ -----Original Message----- From: Peter Dimov via Boost Sent: Friday, May 19, 2017 17:59 To: boost@lists.boost.org Cc: Peter Dimov Subject: Re: [boost] [review] Review of Outcome (starts Fri-19-May)
Would be nice if we could see that demonstrated on godbolt.org.
https://godbolt.org/g/QeHRpl std::error_code f() { return std::error_code( 5, std::system_category() ); } -> ___$ReturnUdt$ = 8 ; size = 4 f PROC call std::_Immortalizestd::_System_error_category mov ecx, DWORD PTR ___$ReturnUdt$[esp-4] mov DWORD PTR [ecx+4], eax mov eax, ecx mov DWORD PTR [ecx], 5 ret 0 f ENDP _______________________________________________ Unsubscribe & other changes: http://lists.boost.org/mailman/listinfo.cgi/boost _______________________________________________ Unsubscribe & other changes: http://lists.boost.org/mailman/listinfo.cgi/boost
The hack is the above. We cache the address of the canonical singleton, ...
Hm. I must be missing something.
BOOSTLITE_NOINLINE inline const std::error_category &_generic_category() { const std::error_category &c = stl11::generic_category(); return c; }
This doesn't cache anything. It just calls stl11::generic_category() and returns the result.
Yes, you're right. Looks like I removed the static storage at some point. I would assume I found I didn't need it any more to achieve the desired removal of code bloat.
The resulting assembler generated is greatly improved on MSVC, a single result<T> shrinks from ~260 opcodes to less than 5.
Would be nice if we could see that demonstrated on godbolt.org.
It's an atomic fence to the *compiler*, not to the CPU. It can't be demonstrated online easily. You need sequences of operations to be folded by an optimiser where the fence inhibits folding. You can have a look at the git history of https://github.com/ned14/boost.outcome/blob/develop/test/constexprs/msvc.csv if you really need proof. Niall -- ned Productions Limited Consulting http://www.nedproductions.biz/ http://ie.linkedin.com/in/nialldouglas/
Niall Douglas wrote:
It's an atomic fence to the *compiler*, not to the CPU. It can't be demonstrated online easily. You need sequences of operations to be folded by an optimiser where the fence inhibits folding.
Or, stated differently, your function just inhibits inlining std::system_category() into user code.
2017-05-19 15:08 GMT+02:00 Niall Douglas via Boost
The constexpr variables are already there. So basically do we prefer:
1. .ensure_empty(), .ensure_value(), .ensure_error() and .ensure_exception()
2. Or .ensure(empty), .ensure(value), .ensure(error) and .ensure(exception)
I figure the latter looked nicer. It's same difference to the compiler, simple overload matching is constant time.
Just one note. If I use namspace prefixes, the notation with "constants" becomes longer:
`o.ensure_empty()` becomes `o.ensure(boost::outcome::empty)`
One could respond to this "just import anything from namespace `boost::outcome` into the scope", but that is imposing on me a certain style of programming, which I not necessarily want to adapt.
Yes that's a good point.
Also, after a few nights of sleeping on it, I'm not keen on .ensure_XXX().
Would people be okay with:
* o.check() <= (void) o.value()
* o.check_error() <= (void) o.error()
* o.check_exception() <= (void) o.exception()
Given that this "ensure" functionality would be used rarely, and some even suggest there is no use case for it, I would be satisfied with this syntax `(void)o.value()`.
Le 19/05/2017 à 15:48, Andrzej Krzemienski via Boost a écrit :
2017-05-19 15:08 GMT+02:00 Niall Douglas via Boost
: The constexpr variables are already there. So basically do we prefer:
1. .ensure_empty(), .ensure_value(), .ensure_error() and .ensure_exception()
2. Or .ensure(empty), .ensure(value), .ensure(error) and .ensure(exception) I figure the latter looked nicer. It's same difference to the compiler, simple overload matching is constant time.
Just one note. If I use namspace prefixes, the notation with "constants" becomes longer:
`o.ensure_empty()` becomes `o.ensure(boost::outcome::empty)`
One could respond to this "just import anything from namespace `boost::outcome` into the scope", but that is imposing on me a certain style of programming, which I not necessarily want to adapt. Yes that's a good point.
Also, after a few nights of sleeping on it, I'm not keen on .ensure_XXX().
Would people be okay with:
* o.check() <= (void) o.value()
* o.check_error() <= (void) o.error()
* o.check_exception() <= (void) o.exception()
Given that this "ensure" functionality would be used rarely, and some even suggest there is no use case for it, I would be satisfied with this syntax `(void)o.value()`. +1 KISS Vicente
2017-05-16 1:47 GMT+02:00 Peter Dimov via Boost
Gavin Lambert wrote:
Maybe I'm missing some context, but unless T is void, making a void get()
method seems very peculiar to me. Logically a get method should actually get something.
get() is not a very good name, it's more like ensure(), and the intended use is in the transition from an outcome<>-using layer to a throwing layer:
T f() // throws { outcome<X> o1 = f1_impl(); // doesn't throw o1.ensure(); // ensure we have a value
outcoume<Y> o2 = f2_impl(); o2.ensure();
return { *o1, *o2 }; }
The reference access paradigm implies that we want to keep the values in the outcomes, so there's no need to actually extract them, we can use them in-place.
In contrast, the value return paradigm implies that in this scenario we want to extract the values from the outcomes and throw the outcomes away:
T f() { X x = f1_impl().value(); Y y = f2_impl().value();
return { x, y }; }
No need for a void get() here. Or, if we want to be verbose:
T f() { outcome<X> o1 = f1_impl(); X x = std::move(o1).value();
outcome<Y> o2 = f2_impl(); Y y = std::move(o2).value();
return { x, y }; }
although in this case o1 and o2 stay in scope and we prefer them not to because we don't want to access them after the values have been extracted. It's more explicit though so to each his own.
It's interesting. Only after Peter's explanation I am able to see the usefulness of outcome<>, at least this part of the interface. It looks like you - Peter - have the understanding of Niall's intentions. And I am clearly lacking them. I would not be able to figure out this use case from the tutorials of the reference. This gives me the impression that the library has a big potential, but it may not succeed because of the failure to explain its power to potential users. It would really be helpful to see a section in the docs with a number of examples simple as the ones above, explaining different use cases. I am still missing how the tribool logic can be used: in if-statements? ``` if (o1 || o2) ... // is that it? ``` Regards, &rzej;
It's interesting. Only after Peter's explanation I am able to see the usefulness of outcome<>, at least this part of the interface. It looks like you - Peter - have the understanding of Niall's intentions. And I am clearly lacking them. I would not be able to figure out this use case from the tutorials of the reference.
This gives me the impression that the library has a big potential, but it may not succeed because of the failure to explain its power to potential users.
This has been a persistent problem over the past year. Once you've been using these things in your own code for a bit, for especially low level systems libraries you'd never willingly go back. The difference is very similar (to me at least) to that feeling you have when you must go back to writing C++ 98 without Boost after you've been using C++ 14 for a quite a while. All that said, 60-70% of C++ would see no benefit to using Outcome nor Expected. Such code is always better off using only C++ exceptions.
It would really be helpful to see a section in the docs with a number of examples simple as the ones above, explaining different use cases.
I am hesitant to add even more length to the existing documentation. It's already long enough to put people off. A refactoring and shortening of the current length would be much more desirable.
I am still missing how the tribool logic can be used: in if-statements?
``` if (o1 || o2) ... // is that it? ```
Outcome provides both binary and ternary conversion operators. Unqualified usage in an if statement will use the binary edition which is true iff state is valued, false otherwise. If you compare an outcome to a tribool, you'll use the ternary edition, there you can test against true, false, and other. If ternary logic suits your use case, using it can make your code much clearer. Other use cases would do better testing .has_value(), .has_error() and .empty() explicitly. Still other use cases would do better to dispatch to a function overload based on the current type of the state. Niall -- ned Productions Limited Consulting http://www.nedproductions.biz/ http://ie.linkedin.com/in/nialldouglas/
On 5/18/17 7:52 AM, Niall Douglas via Boost wrote:
It's interesting. Only after Peter's explanation I am able to see the usefulness of outcome<>, at least this part of the interface. It looks like you - Peter - have the understanding of Niall's intentions. And I am clearly lacking them. I would not be able to figure out this use case from the tutorials of the reference.
This gives me the impression that the library has a big potential, but it may not succeed because of the failure to explain its power to potential users.
This has been a persistent problem over the past year.
This is an exceedingly common problem. It mostly shows up in boost only because boost is one of the few open source libraries which event attempts to document and explain it's libraries. And still, the documentation of our libraries is a very mixed bag. We need to work on this.
Once you've been using these things in your own code for a bit, for especially low level systems libraries you'd never willingly go back. The difference is very similar (to me at least) to that feeling you have when you must go back to writing C++ 98 without Boost after you've been using C++ 14 for a quite a while.
All that said, 60-70% of C++ would see no benefit to using Outcome nor Expected. Such code is always better off using only C++ exceptions.
LOL - which is the same as saying that 60-70% of code would best use exceptions.
It would really be helpful to see a section in the docs with a number of examples simple as the ones above, explaining different use cases.
Another common deficiency in boost library documentation.
I am hesitant to add even more length to the existing documentation. It's already long enough to put people off.
I confess I haven't taken a look at. But this (long) discussion on something that seems so simple is peeked my interest enough to actually spend some time on. Hopefully I'll be able to post a review on this thing in the next few days.
A refactoring and shortening of the current length would be much more desirable.
LOL - which can be said for almost all narrative works. Quantity and quality are orthogonal measures.
I am still missing how the tribool logic can be used: in if-statements?
FWIW - I love tribool
``` if (o1 || o2) ... // is that it? ```
Robert Ramey
2017-05-18 16:52 GMT+02:00 Niall Douglas via Boost
It's interesting. Only after Peter's explanation I am able to see the usefulness of outcome<>, at least this part of the interface. It looks like you - Peter - have the understanding of Niall's intentions. And I am clearly lacking them. I would not be able to figure out this use case from the tutorials of the reference.
This gives me the impression that the library has a big potential, but it may not succeed because of the failure to explain its power to potential users.
This has been a persistent problem over the past year.
Once you've been using these things in your own code for a bit, for especially low level systems libraries you'd never willingly go back. The difference is very similar (to me at least) to that feeling you have when you must go back to writing C++ 98 without Boost after you've been using C++ 14 for a quite a while.
All that said, 60-70% of C++ would see no benefit to using Outcome nor Expected. Such code is always better off using only C++ exceptions.
It would really be helpful to see a section in the docs with a number of examples simple as the ones above, explaining different use cases.
I am hesitant to add even more length to the existing documentation. It's already long enough to put people off.
A refactoring and shortening of the current length would be much more desirable.
I am still missing how the tribool logic can be used: in if-statements?
``` if (o1 || o2) ... // is that it? ```
Outcome provides both binary and ternary conversion operators. Unqualified usage in an if statement will use the binary edition which is true iff state is valued, false otherwise. If you compare an outcome to a tribool, you'll use the ternary edition, there you can test against true, false, and other.
If ternary logic suits your use case, using it can make your code much clearer.
Can you show me the case that ternary logic woud suit and make the code much cleaner?
2017-05-18 16:52 GMT+02:00 Niall Douglas via Boost
It's interesting. Only after Peter's explanation I am able to see the usefulness of outcome<>, at least this part of the interface. It looks like you - Peter - have the understanding of Niall's intentions. And I am clearly lacking them. I would not be able to figure out this use case from the tutorials of the reference.
This gives me the impression that the library has a big potential, but it may not succeed because of the failure to explain its power to potential users.
This has been a persistent problem over the past year.
Once you've been using these things in your own code for a bit, for especially low level systems libraries you'd never willingly go back. The difference is very similar (to me at least) to that feeling you have when you must go back to writing C++ 98 without Boost after you've been using C++ 14 for a quite a while.
I trust your judgement on this. I just wish short examples in the docs could convince me about that.
All that said, 60-70% of C++ would see no benefit to using Outcome nor Expected. Such code is always better off using only C++ exceptions.
That leaves 30-40%, which is a huge market share. I claim about 95% of C++ would see no benefit in using `boost::variant`, but still the library is useful.
It would really be helpful to see a section in the docs with a number of examples simple as the ones above, explaining different use cases.
I am hesitant to add even more length to the existing documentation. It's already long enough to put people off.
I understand your concern.
A refactoring and shortening of the current length would be much more desirable.
It seems likely to me that documentation (the non-reference part) consisting of a number of short annotated exaples could be shorter and convey more information and motivation at the same time. Regards, &rzej;
Once you've been using these things in your own code for a bit, for especially low level systems libraries you'd never willingly go back. The difference is very similar (to me at least) to that feeling you have when you must go back to writing C++ 98 without Boost after you've been using C++ 14 for a quite a while.
I trust your judgement on this. I just wish short examples in the docs could convince me about that.
I did have some examples culled from real world usage in an earlier edition of the tutorial. People found them too hard to grok. They wanted toy, unrealistic examples instead, which is what you have now.
All that said, 60-70% of C++ would see no benefit to using Outcome nor Expected. Such code is always better off using only C++ exceptions.
That leaves 30-40%, which is a huge market share.
If you follow the C++ Reddit on the topic of Either and Maybe monads, in the very long threads of bikeshedding you'll find one fairly widely held opinion that choosing something like Outcome is effectively the same as a library implementation of pre-table EH whereby a fixed overhead is added to all code execution in exchange for various benefits such as very fast handling of failure. Some would also argue that by forcing the programmer to manually write out the logic for handling failure at the point of failure, it improves auditing and maintenance of failure handling. This sort of C++ code, mostly low level systems programming, benefits from using something like Outcome. However a majority of C++ very rarely needs to handle more than one form of failure in any given local use context. For this sort of C++, exceptions prevent cluttering the code with failure handling, and remove that fixed overhead from execution. They're a better choice, a better fit. There are secondary arguments about cost of debugging and bringing code to production quality, but those are hard to prove. Obviously the games and finance industries take a pretty consistent and strong opinion there i.e. globally disable all C++ exceptions, but other users very interested in low latency do make use of C++ exceptions, so if you have talented programmers anything is possible, even easy. I suspect that in games and finance that assuming top notch programmers everywhere in a large org is unwise, so better avoid ruinous surprise by someone messing with code they don't understand. Niall -- ned Productions Limited Consulting http://www.nedproductions.biz/ http://ie.linkedin.com/in/nialldouglas/
Obviously the games and finance industries take a pretty consistent and strong opinion there i.e. globally disable all C++ exceptions,
FWIW this statement is too strong. The amount of C++ in the finance industry is vast , too vast for there to be an consistent no-exceptions theme. Indeed in my little corner it's the other world around - the bulk of code is compiled with exceptions /en/abled.
On 19 May 2017, at 00:04, Niall Douglas via Boost
Once you've been using these things in your own code for a bit, for especially low level systems libraries you'd never willingly go back. The difference is very similar (to me at least) to that feeling you have when you must go back to writing C++ 98 without Boost after you've been using C++ 14 for a quite a while.
I trust your judgement on this. I just wish short examples in the docs could convince me about that.
I did have some examples culled from real world usage in an earlier edition of the tutorial. People found them too hard to grok. They wanted toy, unrealistic examples instead, which is what you have now.
All that said, 60-70% of C++ would see no benefit to using Outcome nor Expected. Such code is always better off using only C++ exceptions.
That leaves 30-40%, which is a huge market share.
If you follow the C++ Reddit on the topic of Either and Maybe monads, in the very long threads of bikeshedding you'll find one fairly widely held opinion that choosing something like Outcome is effectively the same as a library implementation of pre-table EH whereby a fixed overhead is added to all code execution in exchange for various benefits such as very fast handling of failure. Some would also argue that by forcing the programmer to manually write out the logic for handling failure at the point of failure, it improves auditing and maintenance of failure handling. This sort of C++ code, mostly low level systems programming, benefits from using something like Outcome.
However a majority of C++ very rarely needs to handle more than one form of failure in any given local use context. For this sort of C++, exceptions prevent cluttering the code with failure handling, and remove that fixed overhead from execution. They're a better choice, a better fit.
There are secondary arguments about cost of debugging and bringing code to production quality, but those are hard to prove. Obviously the games and finance industries take a pretty consistent and strong opinion there i.e. globally disable all C++ exceptions, but other users very interested in low latency do make use of C++ exceptions, so if you have talented programmers anything is possible, even easy. I suspect that in games and finance that assuming top notch programmers everywhere in a large org is unwise, so better avoid ruinous surprise by someone messing with code they don't understand.
Niall
-- ned Productions Limited Consulting http://www.nedproductions.biz/ http://ie.linkedin.com/in/nialldouglas/
_______________________________________________ Unsubscribe & other changes: http://lists.boost.org/mailman/listinfo.cgi/boost
2017-05-15 13:08 GMT+02:00 Niall Douglas via Boost
you get a C++ exception thrown of type monad_error(no_state).
As a side note, it would be nice from my point of view if you eradicate these last remaining references to 'monad' in the public interface and make that outcome_error (resp. outcome_errc, outcome_category.)
A lot of people grumble at having "monad" in the naming. I don't personally see the problem, it's just a name, and "basic_monad" is exactly what is it: a building block for a monad.
I'll tell you what though, if two more people strongly feel that "basic_monad" and "monad_error" need to be renamed, I will do it. Please people do suggest a better alternative name though.
I strongly feel that name "monad" need to be renamed :) Proposed alternatives: - `basic_monad` -> `outcome_base` or `basic_outcome` - `monad_error` -> `bad_outcome` or `outcome_error` Regards, &rzej;
I'll tell you what though, if two more people strongly feel that "basic_monad" and "monad_error" need to be renamed, I will do it. Please people do suggest a better alternative name though.
I strongly feel that name "monad" need to be renamed :) Proposed alternatives:
- `basic_monad` -> `outcome_base` or `basic_outcome` - `monad_error` -> `bad_outcome` or `outcome_error`
Ok, second vote registered. Niall -- ned Productions Limited Consulting http://www.nedproductions.biz/ http://ie.linkedin.com/in/nialldouglas/
-----Original Message----- From: Boost [mailto:boost-bounces@lists.boost.org] On Behalf Of Andrzej Krzemienski via Boost Sent: 15 May 2017 13:26 To: boost@lists.boost.org Cc: Andrzej Krzemienski Subject: Re: [boost] [review] **NEXT WEEK** Review of Outcome (starts Fri-19-May)
2017-05-15 13:08 GMT+02:00 Niall Douglas via Boost
: you get a C++ exception thrown of type monad_error(no_state).
As a side note, it would be nice from my point of view if you eradicate these last remaining references to 'monad' in the public interface and make that outcome_error (resp. outcome_errc, outcome_category.)
A lot of people grumble at having "monad" in the naming. I don't personally see the problem, it's just a name, and "basic_monad" is exactly what is it: a building block for a monad.
I'll tell you what though, if two more people strongly feel that "basic_monad" and "monad_error" need to be renamed, I will do it. Please people do suggest a better alternative name though.
I strongly feel that name "monad" need to be renamed :) Proposed alternatives:
- `basic_monad` -> `outcome_base` or `basic_outcome` - `monad_error` -> `bad_outcome` or `outcome_error`
+1 monad == FUD :-( basic_monad --> basic_outcome monad_error --> bad_outcome Paul --- Paul A. Bristow Prizet Farmhouse Kendal UK LA8 8AB +44 (0) 1539 561830
I strongly feel that name "monad" need to be renamed :) Proposed alternatives:
- `basic_monad` -> `outcome_base` or `basic_outcome` - `monad_error` -> `bad_outcome` or `outcome_error`
+1
monad == FUD :-(
basic_monad --> basic_outcome
monad_error --> bad_outcome
Ok, that's the third vote. The change will be made. Logged to https://github.com/ned14/boost.outcome/issues/20 Niall -- ned Productions Limited Consulting http://www.nedproductions.biz/ http://ie.linkedin.com/in/nialldouglas/
Niall Douglas wrote:
The major refinement of outcome<T>, result<T> and option<T> over expected
is that you **never** get undefined behaviour when using their observers like you do with expected . So when you call .error() for example, you always get well defined semantics and behaviours. I fully agree with this design decision of yours, by the way. In fact I consider that a defect in expected<>. Why? We have *e that is narrow and value() that is wide. I believe the minimal function for recovering an error must be narrow, as you can want to have the error when you know that there is no value. We could as well have a wide error function, but I expect his use to be used much less
Le 15/05/2017 à 01:35, Peter Dimov via Boost a écrit : than the narrow one. The question is, which exception should the wide error function throw? I've not read the Outcome documentation, but in my opinion it shouldn't be bad_expected_access.
you get a C++ exception thrown of type monad_error(no_state).
As a side note, it would be nice from my point of view if you eradicate these last remaining references to 'monad' in the public interface and make that outcome_error (resp. outcome_errc, outcome_category.)
+1
outcome<Foo> found; // default constructs to empty for(auto &i : container) { auto v = something(i); // returns a result<Foo> if(v) // if not errored { found = std::move(v); // auto upconverts break; } } if(!found) { die(); }
I find this 3-state approach more complex than necessary.
OK, let's go with that. Why not construct 'found' initially to contain some error, instead of being empty? You can even define a special errc constant to denote an empty outcome.
Sure, this will not throw the exception in your earlier example which accessed v.error() when !v, but is the exception really necessary there?
What I'm driving at is that these result types are conceptually (T|E) and the empty state could just be a special case of E.
Or, in more general terms, I feel that there's still much extra weight that can be stripped off (not in terms of sizeof, but in terms of the interface.) +1
Vicente
Vicente J. Botet Escriba wrote:
Le 15/05/2017 à 01:35, Peter Dimov via Boost a écrit :
Niall Douglas wrote:
The major refinement of outcome<T>, result<T> and option<T> over expected
is that you **never** get undefined behaviour when using their observers like you do with expected . So when you call .error() for example, you always get well defined semantics and behaviours. I fully agree with this design decision of yours, by the way. In fact I consider that a defect in expected<>.
Why? We have *e that is narrow and value() that is wide.
I don't like undefined behavior (except when I like it). An accessor that gives you the error without checking first would supposedly save a branch, which I wouldn't consider an acceptable tradeoff even if it did indeed save a branch; but in practice, in correct code the check would already have been made and the compiler would be able to see that. And yes, I don't like *e either.
Documentation: https://ned14.github.io/boost.outcome/index.html
"To whomever it was who proposed the name "Outcome" for the library after a very extended period of name bikeshedding on boost-dev. I had been minded to call the library "Boost.Donkey" just to shut everyone up because the name bike shedding was getting ridiculous. But Outcome is a lot nicer, so thank you to whomever it was." That would be Paul Bristow, in https://lists.boost.org/Archives/boost/2015/05/222687.php
On 14/05/2017 23:09, Peter Dimov via Boost wrote:
That would be Paul Bristow, in
That's an amazing find Peter. I had tried and given up. Thank you. Logged to https://github.com/ned14/boost.outcome/issues/17. And thanks to Paul as well. Niall -- ned Productions Limited Consulting http://www.nedproductions.biz/ http://ie.linkedin.com/in/nialldouglas/
-----Original Message----- From: Boost [mailto:boost-bounces@lists.boost.org] On Behalf Of Niall Douglas via Boost Sent: 15 May 2017 00:36 To: boost@lists.boost.org Cc: Niall Douglas Subject: Re: [boost] [review] **NEXT WEEK** Review of Outcome (starts Fri-19-May)
On 14/05/2017 23:09, Peter Dimov via Boost wrote:
That would be Paul Bristow, in
That's an amazing find Peter. I had tried and given up. Thank you.
Logged to https://github.com/ned14/boost.outcome/issues/17.
And thanks to Paul as well.
You only needed to ask me and I could have told you ;-) Paul
On 12/05/2017 04:19, charleyb123 wrote:
The formal review of Niall Douglas' Outcome library starts next week (Fri-19-May to Sun-28-May). [...] - What is your evaluation of the documentation?
During a quick skim through the docs: in "Expected
During a quick skim through the docs: in "Expected
in Practice" section "The tension between type safety and convenient programming", it's unclear why the final example is any "better" than the ones preceding it.
I had thought the text pretty clear that it isn't better per se, but rather a fusion of the approaches where the type of E is used to prevent code from returning an invalid-for-that-domain error. Some would (and indeed have) call it a worst of both worlds, but it does balance a reasonable amount of getting the compiler to enforce incommensurate error code domains at compile time, with all error codes being subclasses of std::error_code, and thus one can strip the type safety in code further up the call stack such that all error domains feed into a common error_code based error handling system.
In particular, if you're going to explicitly add a constructor to MathError2 such that it knows how to construct itself from MathError1 (as in the final example), why would you not put the switch statement from the first example into that constructor, such that it actually translates the error codes into the expected domain? Especially when there is no loss or overlap in doing so, as in these examples?
If you're writing a switch statement or doing any form of mapping with
error codes, you're doing it wrong from C++ 11 onwards.
One always wants to preserve and propagate exactly the original error
code, unmodified in any way. That's the whole point of
Granted std::error_code does let you transport "foreign" error codes, but surely translating recognised codes from a method's internal implementation to those of its external interface is desirable? (Though perhaps that may depend somewhat on your views of encapsulation and implementation hiding.)
On Sun, May 14, 2017 at 4:33 PM, Niall Douglas via Boost
Indeed I believe that Charley Bay is giving a talk at C++ Now right now on exactly the topic of why we all need to be using more
.
Head check: Is the following function signature "correct" or should it use system_error? template< typename SyncReadStream, typename MutableBufferSequence> std::size_t read( SyncReadStream& s, MutableBufferSequence const& buffers, boost::system::error_code& ec); Thanks
spaketh Vinnie:
<snip>, Head check: Is the following function signature "correct" or should it
use system_error?
template< typename SyncReadStream, typename MutableBufferSequence> std::size_t read( SyncReadStream& s, MutableBufferSequence const& buffers, boost::system::error_code& ec);
That signature is "correct":
(a) It happens to use 'boost::system::error_code', which is a complete
C++03 implementation compatible with C++11's
On 15/05/2017 11:33, Niall Douglas via Boost wrote:
In particular, if you're going to explicitly add a constructor to MathError2 such that it knows how to construct itself from MathError1 (as in the final example), why would you not put the switch statement from the first example into that constructor, such that it actually translates the error codes into the expected domain? Especially when there is no loss or overlap in doing so, as in these examples?
If you're writing a switch statement or doing any form of mapping with error codes, you're doing it wrong from C++ 11 onwards.
One always wants to preserve and propagate exactly the original error code, unmodified in any way. That's the whole point of
.
If you have a class which could have any of a large number of possible implementations (perhaps platform-specific), and as a consumer of that class you don't care which, why would you want it to expose a file_not_found error or a database_key_missing error when as far as you're concerned they're just a key_not_in_dictionary. Don't take that example too literally -- the point is that if the internal implementation is hidden, exposing internal errors seems somewhat meaningless to consumer code. And brittle -- maybe the application code was written to handle the file_not_found case but then the library was changed to use a database backend and started raising database_key_missing errors instead, and now suddenly all the error handling is wrong. Granted the below possibly provides some insulation from that, but it still requires someone to define a POSIX equivalence, and I'm sure there would be cases where the same internal error code logically means different things at the surface API, so such equivalence would be misleading.
lets code with no knowledge of some custom error code domain work with that error code domain, and without recompilation. It's very clever. Both Outcome and Expected are intended to make using error_code much easier and safer for end users than it has been until now.
Only where someone provides a mapping to standard POSIX errors, or just wants to log errors without logic.
2017-05-15 0:54 GMT+02:00 Gavin Lambert via Boost
On 12/05/2017 04:19, charleyb123 wrote:
The formal review of Niall Douglas' Outcome library starts next week (Fri-19-May to Sun-28-May).
[...]
- What is your evaluation of the documentation?
Granted std::error_code does let you transport "foreign" error codes, but surely translating recognised codes from a method's internal implementation to those of its external interface is desirable? (Though perhaps that may depend somewhat on your views of encapsulation and implementation hiding.)
I think, breaking encapsulation in case of reporting failures from functions (be it an exception or an error code) is generally expected and desired, even though nobody will say it explicitly. When I want to communicate with another subsystem, I want the mechanisms of the communications to be abstracted away from me. But when it gets wrong, I want all the information: that I was in fact using the TCP/IP for communication; even exposed to the users, because they may know how to respond. Regards, &rzej;
participants (14)
-
Andrzej Krzemienski
-
charleyb123 .
-
Gavin Lambert
-
Jarrad Waterloo
-
Marc Glisse
-
Niall Douglas
-
Paul A. Bristow
-
Pete Bartlett
-
Peter Dimov
-
Robert Ramey
-
Soul Studios
-
Thijs van den Berg
-
Vicente J. Botet Escriba
-
Vinnie Falco