Boost logo

Boost :

Subject: Re: [boost] [outcome] On the design and documentation
From: Thomas Heller (thom.heller_at_[hidden])
Date: 2017-05-26 10:22:24


On 05/26/2017 09:03 AM, Vicente J. Botet Escriba wrote:
> Le 26/05/2017 à 08:22, Thomas Heller a écrit :
>> On 05/25/2017 07:28 AM, Vicente J. Botet Escriba wrote:
>>> Le 24/05/2017 à 21:44, Thomas Heller via Boost a écrit :
>>
>>>>
>>>> First of all, I don't agree with the strong (conceptual) relationship
>>>> between
>>>> optional (be it boost:: or std::experimental) in such a way that
>>>> expected is a
>>>> generalization of it. From what I understand, the purpose of
>>>> expected/outcome/
>>>> result is to be used as a mechanism to return the result of a
>>>> function. As
>>>> such it should also expose the semantics of it. Fortunately, we
>>>> already have a
>>>> (asynchronous) return object standardized (std::future). And this is
>>>> my basic
>>>> disagreement here: Why not model expected in the lines of future? With
>>>> the
>>>> main points being:
>>>> - expected being movable only
>>>> - expected<T,E>::value() should always return by value and
>>>> invalidate this.
>>>> - (I would really prefer the color .get though ;))
>>>> So the question to Vicente/Niall is: what is the motivation to make it
>>>> "optional-ish"? Do we have use cases which make this design
>>>> undesirable?
>>> expected is a generalization of optional as it is synchronous and could
>>> return more information about why the the value is not there.
>>
>> Right, I can see this argument now, let me try to rephrase it a little
>> bit (correct me if I got something wrong please):
>> We want to be able to have a mechanism to return (or store) a value
>> which might not be there, and we need to know why it is not there. The
>> class that's currently available which almost does what we want is
>> optional, in fact, it is already used in such situations, so what we
>> miss is the possible error. So here we are, and then we naturally end
>> up with something like variant<T, E>. Makes perfect sense.
>>
>> My line of thought was mostly influenced by the property of being
>> solely used as a return object. And well, we already have the
>> asynchronous return objects, so why not go with something a
>> synchronous return object which represents a similar interface/semantics.
>>
>> With that being said, I am still not sure if the result of both ways
>> to look at it should converged into the same result.
>>
>>> expected could be seen as the ready storage of a future.
>>> future::get block until the future is ready and then returns by
>>> reference :)
>>
>> Except not quite ;)
>> excepted indeed sounds like the perfect fit for the value store in the
>> share state. The only problem here, is that it really requires some
>> kind of empty (or uninitialized) since the value is only computed some
>> time in the future (this is motivation #1 for the proposed default
>> constructed semantics), having either a default constructed T or E
>> doesn't make sense in that scenario.
>> So it is more like a variant<monostate, T, E>.
> From a high level I will see it as optional<expected<T,exception_pr>>.
> We optional because we have in addition the not-yet-ready state. Once
> the future becomes ready we have expected<T,exception_ptr>.
> I understand that the implementation optional<expected<T,exception_pr>>
> could be variant<monostate, T, exception_ptr>. The implementation of
> expected<T,exception_pr> could store as well variant<T,exception_pr>,
> but this makes difficult to extract the ready value as
> variant<T,exception_pr> is not convertible from variant<monostate,
> T,exception_pr>. So there are two possibilities for future::then
> * status-quo pass the future as parameter of the continuation
> * make it possible to cast variant<T,exception_pr> from
> variant<monostate, T,exception_pr> and pass expected to the continuation
>
> I proposed other future functions (next and catch_exception) long time
> ago to manage with the success and failure branches, so that we don't
> add any constraint in the way the contents is stored.
>
> Resuming, you are right expected<T,E> will not help as a value type
> representing the ready state without major changes.

Ugh, I don't want to go this path ;)
Mind you, a ready future (which you have present in a continuation)
already fulfills (or should) all the PossiblyValued properties. If it
gets ammended with a has_value/has_error/get_exception function, why
introduce another type? This safes you all the hassle you have with the
different possible conversion paths.

>>
>> The weak point in my proposed interface would indeed be the
>> destructive .value() functions, since a shared state needs to be able
>> to obtain it's value multiple times (in the case of shared_future).
>>
>>>
>>> I want to ask you, what would be the liabilities of an expected that is
>>> copyable?
>>
>> That's a good question. When initially thinking about it, I was very
>> deep into the semantics of asynchronous return objects, which use a
>> common shared state which presents an entirely different problem. So I
>> was always thinking in terms of: Does a copy share the result, like
>> shared_future, or does it have to be unique? I guess this doesn't
>> apply to expected and keeping the value semantics of the underlying
>> types makes most sense.
>> I guess there is nothing wrong with expected being copyable if T and E
>> are copyable.
> Glad to see we agree.
>>
>>> We don't have a problem returning be reference, why would we like to
>>> return by value?
>>
>> Mainly to represent the fact that we have a return value from a
>> function. There is always one return, if you want to alias it, bind it
>> to a named variable.
> I see this as tuples and structured binding. We return a tuple/expected
> and we are able to get the contents by reference. No need to copy while
> you want to access to the elements.

Indeed, no need to copy. Assuming Movable, this would work. I concur
that nowadays you don't necessarily need Movable to return from functions...
In the end, I am fine with either choice.

>>
>>> Why do you prefer get? what do you get with get? How will you name the
>>> function that give you access to the value of a PossiblyValued type?
>>
>> It's really just a different color. The preference to get is coming
>> from my mental model of mine with expected being more like a
>> asynchronous return object (future calls it get as well). There are
>> other types (with similar purpose) using the name get for their
>> accessor: shared_ptr, unique_ptr, tuple and variant.
> + tuple
>
> These get functions are not uniform. Some can throw some not. Some
> return a pointer some return a reference. Some are narrow some are wide.
> Should we use a different name for each flavor? I believe we should,
> but it is too late.
>
> If you had to name a function that takes a unique_ptr and returns the
> pointee value or throw if nullptr, how would you name it.
> I'll name it value() even if value is not a name.
>
> Note that future::get does two things: wait until ready and then
> returns a reference to the success value or throw.
> In some way future::get could be named future::wait_value. Too late also.

I really don't want to get into naming discussions, I'll take whatever
our wizards in the ivory towers decide...

>
>>
>>>>
>>>> By having these constraints, expected of course needs to have an
>>>> uninitialized
>>>> state. As such we'd have the three observers: valid(): true when
>>>> has_value()
>>>> || has_error(), false otherwise (for example default constructed,
>>>> invalidated), has_value() and has_error().
>>> Sorry but, not. We don't need such state. This is something future
>>> needs, but not expected.
>>
>> This state is the logical conclusion coming from the destructive value
>> function. As a result, one could just as well reuse it for default
>> construction. If there is consensus that value returns by reference, I
>> would go for result not being default constructible and have
>> never-empty guarantees.
> I don't follows you here. How the fact that expected<T,E> could be
> default constructible and returning by value or by reference are related.

Assuming this function:
typename std::decay<T>::type expected<T, E>::value()

with the semantics that it invalidates this, you need some way to
represent the "invalid" state. This invalid state could also be reused
when default constructing...

>>
>>> I we decided to default construct to an uninitialized state, I wouldn't
>>> support to show to the user this state via any observable function, but
>>> via UB (as for chrono::duration).
>>
>> That's fine for me as well...
> I've changed my mind since I wrote this as my arguments were wrong.
> Either expected doesn't provide a default constructor or if it does we
> need to default it to T.

I don't like the default to T{}. It seems arbitrary. I would rather see
the default ctor vanish altogether and have the user go the extra mile
when he really needs something like:
expected<T, E> exp(/* the user decides what to do here, be it T{}, or
make_unexpected<E>(....);*/);
{
   // produce expected result here.
}
return exp;


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