Boost logo

Boost :

Subject: Re: [boost] [outcome] Possible extensions/changes to std::experimental::expected
From: Vicente J. Botet Escriba (vicente.botet_at_[hidden])
Date: 2017-05-25 15:32:02


Le 25/05/2017 à 13:18, Andrzej Krzemienski via Boost a écrit :
> 2017-05-25 9:44 GMT+02:00 Vicente J. Botet Escriba via Boost <
> boost_at_[hidden]>:
>
>> Hi,
>>
>> I believe that this private mail I sent to Niall could concern the Boost
>> community and the Outcome review.
>> While the post concerns the proposed std::experimental::expected, this is
>> applicable as well to Outcome.
>>
>> I've added a more points at the end and some additional information about
>> possible open points for std expected.
>>
>> Le 24/05/2017 à 01:04, Vicente J. Botet Escriba a écrit :
>>
>>> Hi,
>>>
>>>
>>> After better understanding of the goals of Boost.Outcome I would like to
>>> share two possible extension of std::experimental::expected
>>>
>>>
>>> 1. Implicit conversion when the errors are convertible and the type is
>>> the same.
>>>
>>> I believe this is a missing constructor.
>>>
>>> EXPLICIT expected<T, E>::expected(expected<T, G>); // E is
>>> convertible from G
>>>
>>> I see that the other constructor is also useful
>>>
>>> EXPLICIT expected<T, E>::expected(expected<U, E>); // T is
>>> convertible from U
>>>
>>> and so
>>>
>>> EXPLICIT expected<T, E>::expected(expected<U, G>); // T is
>>> convertible from U and E is convertible from G
>>>
>>>
>>> We could as well have an adapt function that transforms expected error
>> explicitly to do error propagation
>>
>> adapt : (E->G) x expected<T,E> -> expected<T,G>
>>
>> (E->G) stands for a function that takes an E and return a G.
>>
>> 2. variadic expected<Y, E1, ..., En>
>>> I suggested this already in revision 2 of the Expected standard proposal.
>>>
>>> expected<Y, E1, ..., En> should be something like variant<Y, E1, ..., En>
>>> but with some syntactic sugar.
>>>
>>>
>>> expected<T, E1, ...., En> could be EXPLICIT convertible from expected<T,
>>> G1, ...., Gm> as far as for all Gj we find a unique Ek that is convertible
>>> to.
>>>
>>> We can refine the subsumption relation this could take too long to the
>> compiler (as the previous relation has quadratic complexity as Andrzej
>> reported to me).
>>
>>> I believe this will cover exactly what outcome covers and in addition it
>>> will let open the error types used by the user.
>>>
>>> It acts as a exception specification, which we have abandoned in C++11.
>>>
>>> expected<T, E1, ...., En> f();
>>>
>>> T f() throw (E1, ...., En);
>>>
>>>
>>> We can consider that this is bad, as we have abandoned it.
>>>
>>> Anyway, I think that expected<T, E1, ...., En> is a good generalization
>>> of the proposed outcome<T> where E1 could be none_t, E2 could be error_code
>>> and E3 is exception_ptr.
>>>
>>>
>>> At the end users could use variant<E, E1, ..., En> are return code, so
>> maybe we could to make their life easier.
>>
>>> In this way we could have
>>>
>>> template <class T>
>>>
>>> using option = expected<T, nullopt_t>;
>>>
>>> Note that this is different to std::optional as the default is T not
>>> nullopt_t :(
>>>
>>> I'm not proposing it. Just to say that this could be a possibility for
>> option.
>>
>>> template <class T>
>>>
>>> using result = expected<T, error_code_extended>; // and possibly none.
>>>
>>> template <class T>
>>>
>>> using outcome = expected<T, none_t, error_code_extended, exception_ptr>;
>>>
>>>
>>> I believe we would need the syntactic sugar as expected<T, variant<E1,...
>>> En>> should have a lot of user code noise.
>>>
>>> In addition it will be less efficient than variant<T, E1,... En>.
>>> E.g. expected<T, variant<E1,... En>> should be convertible from
>>> unexpected_type<G> as soon a one of the Ek is convertible from G.
>>>
>>> I believe variant<E1,... En> is not convertible from variant<G1,... Gm>
>>> under the conditions described above, but maybe this should be the case.
>>>
>>> Andrzej told to me that this is already the case for optional<T>, but
>> I've not found yet on which paper this was added?
>>
> Found it. It was through a defect report:
> http://cplusplus.github.io/LWG/lwg-defects.html#2756
Thanks.
>
>
>>> Of course, the never empty warranties could be supported only if the
>>> types Ek allow it and we need to change the standard to ensure it for
>>> exception_ptr.
>>>
>>>
>>> I will not have time to implement a POC for 2, but I believe I could do
>>> it for 1 before Toronto. Anyway I believe it will be worth discussing these
>>> 2 extensions.
>>>
>>>
>>> Best,
>>>
>>> Vicente
>>>
>>>
>>> P.S. EXPLICIT above has the sense given in the standard.
>>>
>>>
>> 3. uninitialized default constructed expected<T,E>
>>
>> I know this could be conflictual, but there are a some advantages to doing
>> it.
>>
>> We don't spend time initializing with something we will assign later.
>>
>> This state corresponds to the moved-from state.
>>
>> Initializing it by default to T() is not better initializing to E().
>>
>> Is expected<Date, error_code> default constructible even if Date is
>> not? Currently it is not the case.
>>
>> There are of course liabilities.
>>
>> We have now the possibility for uninitialized variables, but static
>> analysis tools will help here.
>>
> So, more specifically, I understand that you propose the following:
>
> - Default constructor works: no T or no E is construced (similar what
> outcome<> does)
I was not aware of this. Do you know that from this reference documentation?

constexpr outcome () noexcept(is_nothrow_default_constructible)
      Default constructor.

Sorrty, I couldn't interpret it as you.
But, if it is the case, yes, as outcome does.
> - You can assing to and and destroy such an objetc (similar what
> outcome<> does)
I don't catch what you mean here. Are you referring to the conversion
between expected with convertible value type and error types?
> - You will probably need to add an observer function that checks for
> this singular state, like `is_singular()`. If not for anything else it
> would be used for assisting the static analysis tools. (again, similar what
> outcome<> does)
No. There will be no such observer. This is essential. There is no
visible empty state.
Or do you consider that chrono::duration should tell you if it was
initialized or not?
The user know if it is initialized or not, but there is no tool to check it.
This could be against safe programming, but we are here in C++ and we
should provide first the raw tools and then build on top of them when we
want more.
> - other observer functions (has_value(), value(), has_error(), error())
> cause UB when `is_singular() == true. (this is the only difference from
> outcome<>)
Not the only one.
outcome fixes its error types. I'm proposing an extension to the current
expected so that it can take care of the Outcome use cases and needs.
>
> Did I understand your intentions correctly?
Not completly. I don't want an observer that tells me, attention you
have not initialized your variable, or you have moved from. This is
essential.
>
>> 4. About comparisons
>>
>> Outcome doesn't implement comparisons between Outcomes. He pretend that we
>> don't need them. In addition the mix of comparisons and implicit conversion
>> give us some surprising cases as described in Andrzej blog.
>> [1] A gotcha with Optional - https://akrzemi1.wordpress.com
>> /2014/12/02/a-gotcha-with-optional
>>
>> We have that std::future is not comparable (as exception_ptr is not
>> comparable).
>>
>> Instead of comparison we could specialize std::less<> if we want to use
>> them in ordered containers.
>>
>> What others think?
>>
> I think that ordering expected<> is very rare, and has no intuitive
> semantics: two elements with no value but different error are equivalent or
> not? If a user is determined to store them in maps, or sort them, let her
> provide the ordering predicate manually:
>
> std::set<expected<T>, compare_with_all_unexpected_equivalent> m1;
> std::set<expected<T>, compare_with_unexpected_ordered> m2;
>
> The two predicates could ship with the standard.
This point was open since the beginning. After all a lot of people
wanted to remove it from optional.
I will live with or without. Removing it will reduce a lot the proposal
text and the implementation, but I suspect that some one will need to
define those predicates.
>
>
>> 5. About implicit conversion from T
>>
>> The implicit conversion from T to expected<T,E> is a consequence of
>> wanting that expected<T,E> should behave like a T. But this is not right. A
>> expected<T,E> is not a T.
>>
>> We have in some way that expected<T,E> is explicitly constructible from E
>> bu the use of make_unexpected(e).
>>
>> If we had only and explicit constructor from T the code will be much more
>> uniform. Either you build it with a modified make_expected or with
>> make_unexpected.
>>
>> In my opinion the implicit conversion from T to expected<T> is a mistake.
>>
> It may depend on a personal programming stye. Mine is also in favour of
> explicitness. I even consider conversion from T to optional<T> to be
> dangerous.
Glad to hear that.
>
>>
>> 5. About the exception to throw
>>
>> std::experimental::expected throw bad_expected_access<E>. I adopted the
>> design of optional and bad_optional_access
>>
>> We could have as well a bad_expected_access_base class as Outcome has.
>>
>> My question is why don't throw directly E?
>>
> E may be `std::error_code`. People sometimes writhe `catch (std::exception
> const& e)` to mean "catch evrything (but skip boost::thread_interrupted)".
> This would stop working if we start throwing non-std::exceptions-s.
Except if the user uses E that are std::exceptions ;-)
I don't think your argument is important. The user must know what he does.
>> Some are requesting a way to get a specific exception from E, but either
>> there is one exception that works for all and we can set it using some kind
>> of trait or we need to add a trait parameter to expected to the the mapping
>> :(
>>
>> Do we really want this to be configurable?
>>
> Probably not: the configuration should be reduced to minimum (whatever this
> means).
So if we don't want to configure and don't want to throw E we have just
to throw bad_expected_access<E>.

Note that in order to throw the exception stored inside a exception_ptr
you need some kind of customization or you need to hard code this type
:( This was the case until I removed the exception_ptr as default Error
and as a particular case.
>>
>> 6. Implicit conversion from E outcome::expected<T,E>
>>
>> outcome::expected<T,E> is implicitly convertible from E when there is no
>> risk of ambiguity.
>>
>> std::expected<T,E> is implicitly convertible from unexpected<E>.
>>
>> What people thing we should have? In Boost? in the standard
>
> Can this additional wrapping into unexpected<E> cause negative performance
> effects?
No that I'm aware of. What do you have in mind?
Do you believe that your tagged_bool can cause some negative performance
effects?
If t is the case, we have a problem while raising the level of
abstraction and safety.

Best,
Vicente


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