Boost logo

Boost :

Subject: Re: [boost] [outcome] How to drop the formal empty state
From: Niall Douglas (s_sourceforge_at_[hidden])
Date: 2017-05-26 14:18:13


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;
}
```

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.

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.

In case anyone wishes to study real world code using Outcomes,
https://github.com/ned14/boost.afio/blob/master/include/boost/afio/v2.0/detail/impl/posix/file_handle.ipp#L188
is a use case.

Niall

-- 
ned Productions Limited Consulting
http://www.nedproductions.biz/ http://ie.linkedin.com/in/nialldouglas/

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