$include_dir="/home/hyper-archives/boost/include"; include("$include_dir/msg-header.inc") ?>
Subject: Re: [boost] [outcome] How to drop the formal empty state
From: Gavin Lambert (gavinl_at_[hidden])
Date: 2017-05-26 06:41:14
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.
Also as in the case above, when you forget that an empty state exists:
   result<T> r = something();
   if (r.has_value())
   {
      do_something(r.value());
   }
   else
   {
      log(r.error()); // oops, r might be empty
   }
I dislike gratuitous UB, and Niall assures us that optimisers will 
discard a double check so it should be reasonably cheap.
> Le 26/05/2017 à 01:36, Gavin Lambert a écrit :
>> I don't like the idea of a default-constructed T because T is not 
>> always default-constructible, and this makes it inconsistently behaved 
>> for different T and makes it harder to use uniformly in containers, 
>> especially in generic code.
For the record, not having a default constructor at all also makes it 
harder to use in containers, so I don't like that either.  Though it's a 
weaker dislike than my dislike of a default-constructed T or E.
>> I don't like the idea of a default-constructed E because by convention 
>> (even if not quite in fact as Niall has pointed out -- though I've yet 
>> to see a platform where a 0 error code *didn't* mean success, other 
>> than cases where the formal type is int but is actually used as bool) 
>> the default-constructed error_code means "no error", and this is 
>> heavily reinforced by its operator bool semantics.
>>
>> I do like the idea of a non-default-constructed error code, because 
>> failure to initialise the result does seem like an error to me.  Niall 
>> points out that this is harder to detect and treat specially in code 
>> but I don't agree with that; as long as a suitably unique error code 
>> is used then a simple assert in the error path would pick it up, no 
>> problem.
>>
>> If the consensus is that an initial non-default error code is not 
>> satisfactory, then a formal empty state seems to me like the least 
>> worst alternative.  I just know that it's going to bite someone at 
>> some point.
> 
> If we don't provide a default constructor for expected<T> we could be 
> forced to use optional<expected<T>>.
> 
> This allows to don't pay for this empty state when we don't need it. The 
> problem is that we are paying more than needed when we need it.
> 
> We have two options:
> * we specialize optional<expected<T,E>>
> * we rename the intended specialization xxx<T> is similar to 
> optional<expected<T>>. xxx could be outcome::result or optional_expected
I'm not entirely sure how this relates to what I was saying.
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).
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).
Given that, from the sounds of it an empty state does need to exist in 
the implementation.  Where it sounds like Niall and you differ is 
whether that state should be exposed to the user.  I think if it's there 
anyway then it probably should be, since this enables useful behaviour 
(such as storing in containers and using that state as "method not 
called yet", implying that the empty state should be the 
default-constructed state).
If it turns out that the empty state is not needed by the 
implementation, then a non-default-constructed-E seems like a better 
default value, at least for Outcome where E is a known type.  (It's a 
bit harder for Expected.)
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)?
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.)