Files
boost_variant2/doc/expected.md
T
2017-06-03 15:52:07 +03:00

9.0 KiB

expected<T, E...>

Description

The class expected<T, E...> presented here is an extended version of expected<T, E> as proposed in P0323R1 and the subsequent D0323R2.

The main difference is that this class takes more than one error type, which makes it more flexible. One example of a type of the expected family, outcome, can store either an error of type std::error_code, or an exception in the form of std::exception_ptr. This can be represented naturally in this implementation via expected<T, std::error_code, std::exception_ptr>.

In addition, libraries would generally differ in their choice of error types. It would be a common need in practice of having to combine the results of calling two different libraries, each with its own error type, such as the two examples below:

// Library 1

namespace lib1
{

enum class error
{
    division_by_zero,
    other_error
};

expected<double, error> div( double x, double y );

} // namespace lib1

// Library 2

namespace lib2
{

enum class error
{
    division_by_zero,
    negative_logarithm
};

expected<double, error> log( double x );

} // namespace lib2

In this proposal, combining the results of lib1::div and lib2::log can be achieved via simple composition:

expected<double, lib1::error, lib2::error> log_div_mul( double x, double y, double m )
{
    auto r1 = lib1::div( x, y );
    if( !r1 ) return r1.unexpected();

    auto r2 = lib2::log( r1.value() );
    if( !r2 ) return r2.unexpected();

    return m * r2.value();
}

An alternative approach that requires more effort is also supported:

enum class common_error
{
    division_by_zero,
    negative_logarithm,
    other_error,
    unknown_error
};

common_error make_common_error( lib1::error e );
common_error make_common_error( lib2::error e );

expected<double, common_error> log_div_mul2( double x, double y, double m )
{
    static const auto rm = []( auto x ) { return make_common_error(x); };

    auto r1 = lib1::div( x, y ).remap_errors( rm );
    if( !r1 ) return r1.unexpected();

    auto r2 = lib2::log( r1.value() ).remap_errors( rm );
    if( !r2 ) return r2.unexpected();

    return m * r2.value();
}

When an attempt to access the value via r.value() is made and an error is present, an exception is thrown. By default, this exception is of type bad_expected_access<E>, as in D0323R2, but there are two differences. First, bad_expected_access<E> objects derive from a common base bad_expected_access<void> so that they can be caught at points where the set of possible E is unknown.

Second, the thrown exception can be customized. The implementation calls throw_on_unexpected(e) unqualified, where e is the error object, and the user can define such a function in the namespace of the type of e. Two specialized overloads of throw_on_unexpected are provided, one for std::error_code, which throws the corresponding std::system_error, and one for std::exception_ptr, which rethrows the exception stored in it.

For example, lib1 from above may customize the exceptions associated with lib1::error via the following:

namespace lib1
{

enum class error
{
    division_by_zero,
    other_error
};

class exception: public std::exception
{
private:

    error e_;

public:

    explicit exception( error e ): e_( e ) {}
    virtual const char * what() const noexcept;
};

void throw_on_unexpected( error e )
{
    throw exception( e );
}

} // namespace lib1

In this implementation, unexpected_type<E...> has been called unexpected_<T...> and is an alias for variant<T...>. It is unfortunately not possible to use the name unexpected<T...>, because a function std::unexpected already exists.

The make_... helper functions have been omitted as unnecessary; class template argument deduction as in expected{ 1.0 } or unexpected_{ lib1::division_by_zero } suffices.

Other functions have also been dropped as unnecessary, not providing sufficient value, dangerous, or a combination of the three, although the decision of what to include isn't final at this point. The aim is to produce a minimal interface that still covers the use cases.

expected<T, E1...> can be converted to expected<T, E2...> if all error types in E1... are also in E2.... This allows composition as in the example above. Whether value convertibility ought to also be supported is an open question.

A single monadic operation ("bind") is supported in the form of operator>>, allowing

auto log_div_mul3( double x, double y, double m )
{
    return lib1::div( x, y ) >> [&]( auto && r1 ) {

        return lib2::log( r1 ) >> [&]( auto && r2 ) -> expected<double, lib1::error, lib2::error> {

            return m * r2;

        };
    };
}

as well as the more concise in this example, although limited in utility for real world scenarios,

auto log_div_mul3( double x, double y, double m )
{
    return lib1::div( x, y ) >> std::bind<expected<double, lib1::error, lib2::error>>( lib2::log, _1 ) >> m * _1;
}

The more traditional name then was also a candidate for this operation, but operator>> has two advantages; it avoids the inevitable naming debates and does not require parentheses around the continuation lambda.

Synopsis

// unexpected_

template<class... E> using unexpected_ = variant<E...>;

// bad_expected_access

template<class E = void> class bad_expected_access;

template<> class bad_expected_access<void>: public std::exception
{
public:

    bad_expected_access() noexcept;
    char const * what() const noexcept;
};

template<class E> class bad_expected_access: public bad_expected_access<void>
{
public:

    explicit bad_expected_access( E const& e );
    E error() const;
};

// throw_on_unexpected

template<class E> void throw_on_unexpected( E const& e );
void throw_on_unexpected( std::error_code const& e );
void throw_on_unexpected( std::exception_ptr const& e );

// expected

template<class T, class... E> class expected
{
public:

    // value constructors

    constexpr expected() noexcept( /*see below*/ );

    constexpr expected( T const& t ) noexcept( /*see below*/ );
    constexpr expected( T&& t ) noexcept( /*see below*/ );

    // unexpected constructor

    template<class... E2>
    constexpr expected( unexpected_<E2...> const& x );

    template<class... E2>
    constexpr expected( unexpected_<E2...>&& x );

    // conversion constructor

    template<class... E2>
    constexpr expected( expected<T, E2...> const& x );

    template<class... E2>
    constexpr expected( expected<T, E2...>&& x );

    // emplace

    template<class... A> void emplace( A&&... a );
    template<class V, class... A> void emplace( std::initializer_list<V> il, A&&... a );

    // swap

    void swap( expected& r ) noexcept( /*see below*/ );

    // value queries

    constexpr bool has_value() const noexcept;
    constexpr explicit operator bool() const noexcept;

    // checked value access

    constexpr T& value() &;
    constexpr T const& value() const&;
    constexpr T&& value() &&;
    constexpr T const&& value() const&&;

    // unchecked value access

    T* operator->() noexcept;
    T const* operator->() const noexcept;

    T& operator*() & noexcept;
    T const& operator*() const & noexcept;
    T&& operator*() && noexcept;
    T const&& operator*() const && noexcept;

    // error queries

    template<class E2> constexpr bool has_error() const noexcept;
    constexpr bool has_error() const noexcept;

    // error access

    unexpected_<E...> unexpected() const;

    template<class E2> constexpr E2 error() const noexcept;
    constexpr /*see below*/ error() const noexcept;

    // error mapping

    template<class F> /*see below*/ remap_errors( F&& f ) const;
    expected<T, std::error_code> remap_errors() const;

    // then

    template<class F> /*see below*/ operator>>( F&& f ) const;
};

template<class T, class... E>
inline constexpr bool operator==( expected<T, E...> const& x1, expected<T, E...> const& x2 );

template<class T, class... E>
inline constexpr bool operator!=( expected<T, E...> const& x1, expected<T, E...> const& x2 );

template<class T, class... E>
inline void swap( expected<T, E...>& x1, expected<T, E...>& x2 ) noexcept( /*see below*/ );

// is_expected

template<class T> struct is_expected;

} // namespace variant2
} // namespace boost

Reference

...