Boost logo

Boost :

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


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