Boost logo

Boost :

Subject: Re: [boost] [outcome] How to drop the formal empty state
From: Vicente J. Botet Escriba (vicente.botet_at_[hidden])
Date: 2017-05-26 14:55:44


Le 26/05/2017 à 16:18, Niall Douglas via Boost a écrit :
> On 26/05/2017 07:41, Gavin Lambert via Boost wrote:
>> On 26/05/2017 17:33, Vicente J. Botet Escriba wrote:
>>> std::experimentall:expected<T,E>::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
> the idea of it at
> https://github.com/ned14/boost.afio/blob/master/test/tests/file_handle_create_close/runner.cpp#L34
> 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<none_t, T, E> than a variant<T, E>). 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<T, E> 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<T, E> 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<none_t, T, E> (if that's different)?
> We never change the state unless the user asks explicitly for that. So
> if they move from an expected<T, E> 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
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


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