Boost logo

Boost :

Subject: [boost] Boost.Outcome review - First questions
From: Vicente J. Botet Escriba (vicente.botet_at_[hidden])
Date: 2017-05-21 10:33:57


Hi,

I've tried to read the tutorial, but it is too long for my taste. I will
try it later on :(

A minor issue

    * "As inferred in the example code cases earlier, you can check if
    an |expected<T, E>| has an expected value via a simple test for
    boolean true or more explicitly via |.has_value()| before retrieving
    the value using |.value()|."

This is not correct. It is correct for the de-reference operator, but
.value() don't needs any check with .has_value().

Instead of some deviation, I would expect a report of all deviations and
a rationale explaining why it is better to do this way.

# "Types |T| and |E| cannot be constructible into one another. This is a
fundamental design choice in basic_monad to significantly reduce compile
times so it won't be fixed."

  * "Note though that |expected<void, void>| is permitted (which is
    legal in the LEWG proposal)."

What are the compile time cost |associated the
std::experimental::expected<T,E>?
|

I don't see what |expected<void, void> could mean.
|

# |unexpected_type<E>| is implemented as an |expected<void, E>| and it
lets the basic_monad machinery do the implicit conversion to some
|expected<T, E>| through the less representative to more representative
conversion rules. Note that our Expected passes the LEWG Expected test
suite just fine, so you probably won't notice this implementation detail
in your code.

The test of my POC implementation couldn't be complete, so this is not a
sign of compatibility.

Which operation allow to convert expected<void,E> to expected<T,E> and
what is the expected behavior if expected<void,E> has a value?

# Our |expected<T, E>| defaults E to |std::error_code| rather than
|std::error_condition| (the LEWG proposal fixes this in P0323R2).

I don't have a strong opinion on which default would be the best. As you
suggested, I have added it to the next revision.

We don't implement the ordering and hashing operator overloads due to
https://akrzemi1.wordpress.com/2014/12/02/a-gotcha-with-optional/. The
fact the LEWG proposal does as currently proposed is a defect (it will
be discussed by LEWG with P0323R2 in Toronto).

As for optional this could be considered a defect. The question is if we
want expected to differen from the optional interface.

If we considered it a defect, I don't believe that we should remove
comparison, but IMHO we should make the constructor from T explicit.

I don't expect the committee to fix this for expected and let the issue
for optional. We will see.

# We don't implement |make_expected_from_call()| as we think it highly
likely to be removed from the next version of the proposal due to it
conferring little value.

This is possible and the function can be implemented very easily. I can
live with and without it.

# Our |make_expected()| and |make_unexpected()| functions deviate
significantly as we believe the LEWG ones to be defective in various
ways. This topic will be discussed in Toronto and reconciliation later
is highly likely.

Please, could you tell how those are defective so that we can fix them?
Otherwise I don't see what we could discuss in Toronto.

# Our |value_or()| member functions pass through the reference rather than
returning by value (we don't understand why the LEWG proposal does
except that |std::optional<T>| does, which itself is a bad design choice
and it should be changed).

Well, what is the advantage to returning by reference?

Rather than |void|, our |value_type| and |error_type| typedefs are set
to a descriptive compiler error generating type if the Expected is
configured with |void|. If you want to know if no type was configured,
use the static constexpr bools at |expected<T, E>::has_value_type| and
|expected<T, E>::has_error_type| or use |raw_value_type| and
|raw_error_type|. The reason for this deviation is to make
metaprogramming using Outcome's transports much easier and less error
prone.

Sorry. I don't understand what is the issue. Could you elaborate?

# Our Expected always defines the default, copy and move constructors even
if the the type configured is not capable of it. That means
|std::is_copy_constructible| etc returns true when they should return
false. The reason why is again to significantly improve compile times by
hugely reducing the number of templates which need to be instantiated
during routine basic_monad usage, and again this won't be fixed. Instead
use the static constexpr bools at:

Defining these operation when they can not be implemented means that you
cannot use SFINAE on this type. Maybe I'm wrong, but I believe we want
to check if a type is constructible, ... using the C++ standard type
traits.

Next follow the first questions I have mainly from the reference
documentation of expected<T,E>.

In general I find most of the operation not specified enough. I would
like to have something in line with the standard way of specifying an
interface.

* it is not clear from the reference documentation if expected<void,E>
is valid?

|* |is expected<T,E> the sum type of T, E and empty? and exception_ptr?

|"outcome<T>| can be empty, or hold an instance of a type |T|, or hold
an instance of |error_code_extended|, or hold an instance of
|std::exception_ptr|. This saves you having to wrap an error code into
an exception pointer instance, thus also avoiding a memory allocation."

std::experimental::expected requires that its Error type to be no throw
constructible, copyable and movable.

What are the exception warranties for |error_code_extended?
|

What are the exception warranties for |outcome<T>||?
|

* inplace construction

What is the difference between:

constexpr expected (value_t _)
noexcept(std::is_nothrow_default_constructible< value_type >::value)
      Implicit constructor of a valued monad (default constructed)

and

constexpr expected (in_place_t, Args &&... args)
noexcept(std::is_nothrow_constructible< value_type, Arg, Args... >::value)

when there is no Args. IMO the second subsumes the first and so there is
no need fro the first overload and the value_t tag.

* default constructor

constexpr expected () noexcept(is_nothrow_default_constructible)
      Default constructor.

This doesn't say what the default constructor does.

* Shouldn't

constexpr expected (in_place_t, std::initializer_list< U > l)
noexcept(std::is_nothrow_constructible< value_type,
std::initializer_list< U >>::value)
      Implicit constructor from an initializer list.

be

   constexpr explicit expected(in_place_t, initializer_list<U> il,
Args&&... args);

* What is void_rebound and where it is defined

constexpr expected (const void_rebound &v)
noexcept(std::is_nothrow_copy_constructible< error_type >::value)
      Implicit constructor from an identically configured
basic_monad<void> by copy.

* construction from error type

What is wrong with

     template <class... Args>
         constexpr explicit expected(unexpect_t, Args&&...);
     template <class U, class... Args>
         constexpr explicit expected(unexpect_t, initializer_list<U>,
Args&&...);

Why do you prefer an implicit conversion. This deviation must be in the
rationale of the differences.

constexpr expected (const error_type &v)
noexcept(std::is_nothrow_copy_constructible< error_type >::value)
      Implicit constructor from a error_type by copy.

constexpr expected (error_type &&v)
noexcept(std::is_nothrow_move_constructible< error_type >::value)

* raw types

It is not clear what is the difference between xxx and raw_xxx

typedef value_storage_type::value_type value_type
      The type potentially held by the monad.

typedef implementation_policy::value_type raw_value_type
      The raw type potentially held by the monad.

typedef value_storage_type::error_type error_type
      The error code potentially held by the monad.

typedef implementation_policy::error_type raw_error_type
      The raw error code potentially held by the monad.

typedef value_storage_type::exception_type exception_type
      The exception ptr potentially held by the monad.

typedef implementation_policy::exception_type raw_exception_type
      The raw exception ptr potentially held by the monad.

* What is the "The exception ptr potentially held by the monad"?

* what is the sizeof expected <T,E>?

* is basic_monad something proposed or an implementation detail?

If the first, this should be renamed, if the second this class should
not appear in the documentation and the word monad should be banned.

* What is expected<Policy> in

template<class Policy , typename = typename
std::enable_if<std::is_same<typename implementation_policy::value_type,
typename Policy::value_type>::value || std::is_void<typename
Policy::value_type>::value || std::is_constructible<typename
implementation_policy::value_type, typename
Policy::value_type>::value>::type, typename = typename
std::enable_if<std::is_same<typename implementation_policy::error_type,
typename Policy::error_type>::value || std::is_constructible<typename
implementation_policy::error_type, typename
Policy::error_type>::value>::type, typename = typename
std::enable_if<std::is_same<typename
implementation_policy::exception_type, typename
Policy::exception_type>::value || std::is_constructible<typename
implementation_policy::exception_type, typename
Policy::exception_type>::value>::type>
constexpr expected (expected< Policy > &&o,
explicit_conversion_from_different_policy=explicit_conversion_from_different_policy())
      Explicit move constructor from a basic_monad with a differing
implementation policy. For this constructor to be available, value_type,
error_type and exception_type must be identical or constructible.

* move constructor/copy constructor

The reference documentation doesn't says what this does.

* Missing move/copy assignment

* bool conversion should be explicit

* is_ready has nothing to be with expected.

* Shouldn't value_or return by value? otherwise what would be the
difference between get_or and value_or?

* emplace. Missing overload

     template <class U, class... Args>
         void emplace(initializer_list<U>, Args&&...);

* swap: What is the meaning of swapping an expected with an option?

swap must follow the standard signature.

* Missing preconditions on the observers, which make them wide
operations. Saying that they return an address makes them wide
operations however accessing to the contents of this address will be
undefined behavior.

I prefer the standard way, defining a narrow function and stating
clearly the pre-conditions.

* what error_or
<https://ned14.github.io/boost.outcome/structboost_1_1outcome_1_1v1__xxx_1_1policy_1_1expected__policy__base.html#ae0882c7d380b88b9011bcc60c59903bd>
returns if it contains an exception?

* what is the difference between get_error_or and error_or?

https://github.com/ned14/boost.outcome/issues/19

* Missing overload for get_unexpected. In addition it doesn't return an
unexpected_type.

constexpr const E & get_unexpected () const;

Best,

Vicente


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