Boost logo

Boost :

Subject: Re: [boost] [outcome] Possible extensions/changes to std::experimental::expected
From: Andrzej Krzemienski (akrzemi1_at_[hidden])
Date: 2017-05-25 11:18:59


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

>> 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)
   - You can assing to and and destroy such an objetc (similar what
   outcome<> does)
   - 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)
   - other observer functions (has_value(), value(), has_error(), error())
   cause UB when `is_singular() == true. (this is the only difference from
   outcome<>)

Did I understand your intentions correctly?

> 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.

> 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.

>
>
> 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.

>
> 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).

>
>
> 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?

Regards,
&rzej;


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