On 26/05/2017 17:33, Vicente J. Botet Escriba wrote:
std::experimentall:expected
::error, doesn't throws. I don't see a use case where we want to retrieve an error without checking before. Maybe you have a case. Perhaps unit tests, where you're expecting an error but the code unexpectedly succeeds. Very true. I have a next gen unit testing infrastructure built around Outcomes where UB on any of the observers would be a no-go. You can see On 26/05/2017 07:41, Gavin Lambert via Boost wrote: the idea of it at https://github.com/ned14/boost.afio/blob/master/test/tests/file_handle_creat... and note how self describing the tables of success and failure are.
At least in terms of storage, the current implementation of empty state is presumably free (it should be no more expensive to internally store a variant
than a variant ). And it's currently required to exist due to exception guarantees (and possible noexcept(false) move constructors). As mentioned in another discussion thread, the empty state is also used internally as a micro-optimisation. So it would likely remain internally whatever the decision taken here, as a tool for making the CPU expend identical CPU cycles on both positive and negative branches on state. Again, if people don't like that behaviour of outcome/result to be equally costly on T or E branches chosen, expected
never has an empty state and therefore always naturally favours the E state (because in .value() you check for an errored state, and if so throw an exception, so returning a value is usually the branch the compiler generates a branch to later code for). I don't think that T should be restricted to noexcept(true)-movable types only, as this prevents using it with C++03 non-POD types (that have a copy constructor but lack a move constructor), which are still likely to be widespread in codebases (although perhaps less common as return values). I couldn't agree more about type T. But Expected does not demand nothrow move construction from type T, only type E. And usually most of the time the end user will control the source code for any type E used. It's a fair restriction in exchange for never empty.
Another consideration is that regardless of default construction or not is that you need to decide what an expected
will contain if someone moves-from it (directly). Is it now formal-empty or does it now contain a moved-from-T or moved-from-E? Or does it contain a moved-from-variant (if that's different)? We never change the state unless the user asks explicitly for that. So if they move from an expected and the state was an E, it remains an E, just a moved-from E. Resetting to empty looks attractive, but I found out the hard way it is a bad design decision. Code consuming a rvalue reference does not actually have to move anything, nothing in the C++ standard says it does. It's only a widely held convention that it ought to.
(move constructors/assignment don't actually have to move. libstdc++'s std::string famously didn't for example)
The return type of value() plays a role here as well. If it returns by value, then you can probably pick whatever you like. If it returns by reference, then the caller can now move-from the internal T and ensure it will be in the has-a-moved-from-T state, not the empty state. (Which may or may not be desirable, but implies that moved-from is not the same as empty, which might surprise users of smart pointers.)
(Returning by reference also disallows possible future storage optimisations from nested variant merging, as mentioned in another thread.) Reference returning .value() I've found in real world usage to be surprisingly useful.
Back when I began using Outcome, I used to write code something like:
``` result<Foo> something() { Foo ret; ... build ret ... return make_valued_result<Foo>(std::move(ret)); } ```
And that's probably what first time users are going to write.
But after I built up some experience, now I tend to write this instead:
``` result<Foo> something() { result<Foo> ret(Foo()); Foo &foo = ret.value(); ... build foo ... return ret; } ``` But here you cannot have an empty result, so expected will be more adapted here. And as you are sure you have a value, so you will use * instead of value. I can tell people are going to ask me: why the second form instead of the first form?
That's surprisingly hard to answer in a meaningful way. I guess the former form you are writing code to generate a Foo, and then wrapping that Foo up into result<Foo> for the purposes of indicating success. The second form you are being more specific, you are not generating a Foo, you are generating a result<Foo>. Somehow the code feels right with the second form. It's somehow more idiomatic. The problem with this idiom is that you have a reference to something
Le 26/05/2017 à 16:18, Niall Douglas via Boost a écrit : that could become an error :( We are introducing with this idiom a possibly reference leak. But this is C++.
I appreciate that's very vague. But my point is, the reference returning .value() makes a lot of sense. You will end up using these objects to build returned values a **lot** in the code you write.
I would prefer to program it as the first form and let the compiler do the needed optimizations :) But sometimes we cannot forget the time. Vicente