P0323R10
std::expected

Published Proposal,

This version:
https://wg21.link/P0323R10
Authors:
(Nokia)
(Woven Planet)
Audience:
LWG
Project:
ISO/IEC JTC1/SC22/WG21 14882: Programming Language — C++
Source:
github.com/jfbastien/papers/blob/master/source/P0323R10.bs

Class template expected<T, E> is a vocabulary type which contains an expected value of type T, or an error E. The class skews towards behaving like a T, because its intended use is when the expected type is contained. When something unexpected occurs, more typing is required. When all is good, code mostly looks as if a T were being handled.

Class template expected<T, E> contains either:

The interface can be queried as to whether the underlying value is the expected value (of type T) or an unexpected value (of type E). The original idea comes from Andrei Alexandrescu C++ and Beyond 2012: Systematic Error Handling in C++ Alexandrescu.Expected, which he revisited in CppCon 2018, including mentions of this paper.

The interface and the rational are based on std::optional [N3793]. We consider expected<T, E> as a supplement to optional<T>, expressing why an expected value isn’t contained in the object.

1. Revision History

This paper is an update to [P0323r9], which was wording-ready but targetted the Library Fundamentals TS v3.

In the 2021-04-06 LEWG telecon, LEWG decided to target the IS instead of a TS.

Poll: P0323r9 should add a feature test macro, change the namespace/header from experimental to std to be re-targeted to the IS, forward it to electronic polling to forward to LWG

SF F N A SA
6 6 5 1 0

Related papers:

2. Motivation

C++'s two main error mechanisms are exceptions and return codes. Characteristics of a good error mechanism are:

  1. Error visibility: Failure cases should appear throughout the code review: debugging can be painful if errors are hidden.

  2. Information on errors: Errors should carry information from their origin, causes and possibly the ways to resolve it.

  3. Clean code: Treatment of errors should be in a separate layer of code and as invisible as possible. The reader could notice the presence of exceptional cases without needing to stop reading.

  4. Non-Intrusive error: Errors should not monopolize the communication channel dedicated to normal code flow. They must be as discrete as possible. For instance, the return of a function is a channel that should not be exclusively reserved for errors.

The first and the third characteristic seem contradictory and deserve further explanation. The former points out that errors not handled should appear clearly in the code. The latter tells us that error handling must not interfere with the legibility, meaning that it clearly shows the normal execution flow.

Here is a comparison between the exception and return codes:

Exception Return error code
Visibility Not visible without further analysis of the code. However, if an exception is thrown, we can follow the stack trace. Visible at the first sight by watching the prototype of the called function. However ignoring return code can lead to undefined results and it can be hard to figure out the problem.
Informations Exceptions can be arbitrarily rich. Historically a simple integer. Nowadays, the header provides richer error code.
Clean code Provides clean code, exceptions can be completely invisible for the caller. Force you to add, at least, a if statement after each function call.
Non-Intrusive Proper communication channel. Monopolization of the return channel.

We can do the same analysis for the expected<T, E> class and observe the advantages over the classic error reporting systems.

  1. Error visibility: It takes the best of the exception and error code. It’s visible because the return type is expected<T, E> and users cannot ignore the error case if they want to retrieve the contained value.

  2. Information: Arbitrarily rich.

  3. Clean code: The monadic interface of expected provides a framework delegating the error handling to another layer of code. Note that expected<T, E> can also act as a bridge between an exception-oriented code and a nothrow world.

  4. Non-Intrusive: Use the return channel without monopolizing it.

Other notable characteristics of expected<T, E> include:

2.1. Sample Usecase

The following is how WebKit-based browsers parse URLs and use std::expected to denote failure:

template<typename CharacterType>
Expected<uint32_t, URLParser::IPv4PieceParsingError> URLParser::parseIPv4Piece(CodePointIterator<CharacterType>& iterator, bool& didSeeSyntaxViolation)
{
    enum class State : uint8_t {
        UnknownBase,
        Decimal,
        OctalOrHex,
        Octal,
        Hex,
    };
    State state = State::UnknownBase;
    Checked<uint32_t, RecordOverflow> value = 0;
    if (!iterator.atEnd() && *iterator == '.')
        return makeUnexpected(IPv4PieceParsingError::Failure);
    while (!iterator.atEnd()) {
        if (isTabOrNewline(*iterator)) {
            didSeeSyntaxViolation = true;
            ++iterator;
            continue;
        }
        if (*iterator == '.') {
            ASSERT(!value.hasOverflowed());
            return value.unsafeGet();
        }
        switch (state) {
        case State::UnknownBase:
            if (UNLIKELY(*iterator == '0')) {
                ++iterator;
                state = State::OctalOrHex;
                break;
            }
            state = State::Decimal;
            break;
        case State::OctalOrHex:
            didSeeSyntaxViolation = true;
            if (*iterator == 'x' || *iterator == 'X') {
                ++iterator;
                state = State::Hex;
                break;
            }
            state = State::Octal;
            break;
        case State::Decimal:
            if (!isASCIIDigit(*iterator))
                return makeUnexpected(IPv4PieceParsingError::Failure);
            value *= 10;
            value += *iterator - '0';
            if (UNLIKELY(value.hasOverflowed()))
                return makeUnexpected(IPv4PieceParsingError::Overflow);
            ++iterator;
            break;
        case State::Octal:
            ASSERT(didSeeSyntaxViolation);
            if (*iterator < '0' || *iterator > '7')
                return makeUnexpected(IPv4PieceParsingError::Failure);
            value *= 8;
            value += *iterator - '0';
            if (UNLIKELY(value.hasOverflowed()))
                return makeUnexpected(IPv4PieceParsingError::Overflow);
            ++iterator;
            break;
        case State::Hex:
            ASSERT(didSeeSyntaxViolation);
            if (!isASCIIHexDigit(*iterator))
                return makeUnexpected(IPv4PieceParsingError::Failure);
            value *= 16;
            value += toASCIIHexValue(*iterator);
            if (UNLIKELY(value.hasOverflowed()))
                return makeUnexpected(IPv4PieceParsingError::Overflow);
            ++iterator;
            break;
        }
    }
    ASSERT(!value.hasOverflowed());
    return value.unsafeGet();
}

These results are then accumulated in a vector , and different failure conditions are handled differently. An important fact to internalize is that the first failure encountered isn’t necessarily the one which is returned, which is why exceptions aren’t a good solution here: parsing must continue.

template<typename CharacterTypeForSyntaxViolation, typename CharacterType>
Expected<URLParser::IPv4Address, URLParser::IPv4ParsingError> URLParser::parseIPv4Host(const CodePointIterator<CharacterTypeForSyntaxViolation>& iteratorForSyntaxViolationPosition, CodePointIterator<CharacterType> iterator)
{
    Vector<Expected<uint32_t, URLParser::IPv4PieceParsingError>, 4> items;
    bool didSeeSyntaxViolation = false;
    if (!iterator.atEnd() && *iterator == '.')
        return makeUnexpected(IPv4ParsingError::NotIPv4);
    while (!iterator.atEnd()) {
        if (isTabOrNewline(*iterator)) {
            didSeeSyntaxViolation = true;
            ++iterator;
            continue;
        }
        if (items.size() >= 4)
            return makeUnexpected(IPv4ParsingError::NotIPv4);
        items.append(parseIPv4Piece(iterator, didSeeSyntaxViolation));
        if (!iterator.atEnd() && *iterator == '.') {
            ++iterator;
            if (iterator.atEnd())
                syntaxViolation(iteratorForSyntaxViolationPosition);
            else if (*iterator == '.')
                return makeUnexpected(IPv4ParsingError::NotIPv4);
        }
    }
    if (!iterator.atEnd() || !items.size() || items.size() > 4)
        return makeUnexpected(IPv4ParsingError::NotIPv4);
    for (const auto& item : items) {
        if (!item.hasValue() && item.error() == IPv4PieceParsingError::Failure)
            return makeUnexpected(IPv4ParsingError::NotIPv4);
    }
    for (const auto& item : items) {
        if (!item.hasValue() && item.error() == IPv4PieceParsingError::Overflow)
            return makeUnexpected(IPv4ParsingError::Failure);
    }
    if (items.size() > 1) {
        for (size_t i = 0; i < items.size() - 1; i++) {
            if (items[i].value() > 255)
                return makeUnexpected(IPv4ParsingError::Failure);
        }
    }
    if (items[items.size() - 1].value() >= pow256(5 - items.size()))
        return makeUnexpected(IPv4ParsingError::Failure);

    if (didSeeSyntaxViolation)
        syntaxViolation(iteratorForSyntaxViolationPosition);
    for (const auto& item : items) {
        if (item.value() > 255)
            syntaxViolation(iteratorForSyntaxViolationPosition);
    }

    if (UNLIKELY(items.size() != 4))
        syntaxViolation(iteratorForSyntaxViolationPosition);

    IPv4Address ipv4 = items.takeLast().value();
    for (size_t counter = 0; counter < items.size(); ++counter)
        ipv4 += items[counter].value() * pow256(3 - counter);
    return ipv4;
}

2.2. Error retrieval and correction

The major advantage of expected<T, E> over optional<T> is the ability to transport an error. Programmer do the following when a function call returns an error:

  1. Ignore it.

  2. Delegate the responsibility of error handling to higher layer.

  3. Try to resolve the error.

Because the first behavior might lead to buggy application, we ignore the usecase. The handling is dependent of the underlying error type, we consider the exception_ptr and the errc types.

2.3. Impact on the standard

These changes are entirely based on library extensions and do not require any language features beyond what is available in C++20.

3. Design rationale

The same rationale described in [N3672] for optional<T> applies to expected<T, E> and expected<T, nullopt_t> should behave almost the same as optional<T> (though we advise using optional in that case). The following sections presents the rationale in N3672 applied to expected<T, E>.

3.1. Conceptual model of expected<T, E>

expected<T, E> models a discriminated union of types T and unexpected<E>. expected<T, E> is viewed as a value of type T or value of type unexpected<E>, allocated in the same storage, along with observers to determine which of the two it is.

The interface in this model requires operations such as comparison to T, comparison to E, assignment and creation from either. It is easy to determine what the value of the expected object is in this model: the type it stores (T or E) and either the value of T or the value of E.

Additionally, within the affordable limits, we propose the view that expected<T, E> extends the set of the values of T by the values of type E. This is reflected in initialization, assignment, ordering, and equality comparison with both T and E. In the case of optional<T>, T cannot be a nullopt_t. As the types T and E could be the same in expected<T, E>, there is need to tag the values of E to avoid ambiguous expressions. The unexpected(E) deduction guide is proposed for this purpose. However T cannot be unexpected<E> for a given E.

expected<int, string> ei = 0;
expected<int, string> ej = 1;
expected<int, string> ek = unexpected(string());

ei = 1;
ej = unexpected(E());;
ek = 0;

ei = unexpected(E());;
ej = 0;
ek = 1;

3.2. Default E template paremeter type

At the Toronto meeting LEWG decided against having a default E template parameter type (std::error_code or other). This prevents us from providing an expected deduction guides for error construction which is a good thing: an error was not expected, a T was expected, it’s therefore sensible to force spelling out unexpected outcomes when generating them.

3.3. Initialization of expected<T, E>

In cases where T and E have value semantic types capable of storing n and m distinct values respectively, expected<T, E> can be seen as an extended T capable of storing n + m values: these T and E stores. Any valid initialization scheme must provide a way to put an expected object to any of these states. In addition, some Ts aren’t CopyConstructible and their expected variants still should be constructible with any set of arguments that work for T.

As in [N3672], the model retained is to initialize either by providing an already constructed T or a tagged E. The default constructor required T to be default-constructible (since expected<T> should behave like T as much as possible).

string s"STR";

expected<string, errc> es{s}; // requires Copyable<T>
expected<string, errc> et = s; // requires Copyable<T>
expected<string, errc> ev = string"STR"; // requires Movable<T>

expected<string, errc> ew; // expected value
expected<string, errc> ex{}; // expected value
expected<string, errc> ey = {}; // expected value
expected<string, errc> ez = expected<string,errc>{}; // expected value

In order to create an unexpected object, the deduction guide unexpected needs to be used:

expected<string, int> ep{unexpected(-1)}; // unexpected value, requires Movable<E>
expected<string, int> eq = unexpected(-1); // unexpected value, requires Movable<E>

As in [N3672], and in order to avoid calling move/copy constructor of T, we use a “tagged” placement constructor:

expected<MoveOnly, errc> eg; // expected value
expected<MoveOnly, errc> eh{}; // expected value
expected<MoveOnly, errc> ei{in_place}; // calls MoveOnly{} in place
expected<MoveOnly, errc> ej{in_place, "arg"}; // calls MoveOnly{"arg"} in place

To avoid calling move/copy constructor of E, we use a “tagged” placement constructor:

expected<int, string> ei{unexpect}; // unexpected value, calls string{} in place
expected<int, string> ej{unexpect, "arg"}; // unexpected value, calls string{"arg"} in place

An alternative name for in_place that is coherent with unexpect could be expect. Being compatible with optional<T> seems more important. So this proposal doesn’t propose such an expect tag.

The alternative and also comprehensive initialization approach, which is compatible with the default construction of expected<T, E> as T(), could have been a variadic perfect forwarding constructor that just forwards any set of arguments to the constructor of the contained object of type T.

3.4. Never-empty guarantee

As for boost::variant<T, unexpected<E>>, expected<T, E> ensures that it is never empty. All instances v of type expected<T, E> guarantee v has constructed content of one of the types T or E, even if an operation on v has previously failed.

This implies that expected may be viewed precisely as a union of exactly its bounded types. This "never-empty" property insulates the user from the possibility of undefined expected content or an expected valueless_by_exception as std::variant and the significant additional complexity-of-use attendant with such a possibility.

In order to ensure this property the types T and E must satisfy the requirements as described in [P0110R0]. Given the nature of the parameter E, that is, to transport an error, it is expected to be is_nothrow_copy_constructible<E>, is_nothrow_move_constructible<E>, is_nothrow_copy_assignable<E> and is_nothrow_move_assignable<E>.

Note however that these constraints are applied only to the operations that need them.

If is_nothrow_constructible<T, Args...> is false, the expected<T, E>::emplace(Args...) function is not defined. In this case, it is the responsibility of the user to create a temporary and move or copy it.

3.5. The default constructor

Similar data structure includes optional<T>, variant<T1,...,Tn> and future<T>. We can compare how they are default constructed.

This raises several questions about expected<T, E>:

Requiring E to be default constructible seems less constraining than requiring T to be default constructible (e.g. consider the Date example in [N3527]). With the same semantics expected<Date,E> would be Regular with a meaningful not-a-date state created by default.

The authors consider the arguments in [N3527] valid for optional<T> and expected<T, E>, however the committee requested that expected<T, E> default constructor should behave as constructed with T() if T is default constructible.

3.6. Could Error be void

void isn’t a sensible E template parameter type: the expected<T, void> vocabularity type means "I expect a T, but I may have nothing for you". This is literally what optional<T> is for. If the error is a unit type the user can use std::monostate or std::nullopt.

3.7. Conversion from T

An object of type T is implicitly convertible to an expected object of type expected<T, E>:

expected<int, errc> ei = 1; // works

This convenience feature is not strictly necessary because you can achieve the same effect by using tagged forwarding constructor:

expected<int, errc> ei{in_place, 1};

It has been demonstrated that this implicit conversion is dangerous [a-gotcha-with-optional].

An alternative will be to make it explicit and add a success<T> (similar to unexpected<E> explicitly convertible from T and implicitly convertible to expected<T, E>.

expected<int, errc> ei = success(1);
expected<int, errc> ej = unexpected(ec);

The authors consider that it is safer to have the explicit conversion, the implicit conversion is so friendly that we don’t propose yet an explicit conversion. In addition std::optional has already be delivered in C++17 and it has this gotcha.

Further, having success makes code much more verbose than the current implicit conversion. Forcing the usage of success would make expected a much less useful vocabulary type: if success is expected then success need not be called out.

3.8. Conversion from E

An object of type E is not convertible to an unexpected object of type expected<T, E> since E and T can be of the same type. The proposed interface uses a special tag unexpect and unexpected deduction guide to indicate an unexpected state for expected<T, E>. It is used for construction and assignment. This might raise a couple of objections. First, this duplication is not strictly necessary because you can achieve the same effect by using the unexpect tag forwarding constructor:

expected<string, errc> exp1 = unexpected(1);
expected<string, errc> exp2 = {unexpect, 1};
exp1 = unexpected(1);
exp2 = {unexpect, 1};

or simply using deduced template parameter for constructors

expected<string, errc> exp1 = unexpected(1);
exp1 = unexpected(1);

While some situations would work with the {unexpect, ...} syntax, using unexpected makes the programmer’s intention as clear and less cryptic. Compare these:

expected<vector<int>, errc> get1() {}
    return {unexpect, 1};
}
expected<vector<int>, errc> get2() {
    return unexpected(1);
}
expected<vector<int>, errc> get3() {
    return expected<vector<int>, int>{unexpect, 1};
}
expected<vector<int>, errc> get2() {
    return unexpected(1);
}

The usage of unexpected is also a consequence of the adapted model for expected: a discriminated union of T and unexpected<E>.

3.9. Should we support the exp2 = {}?

Note also that the definition of unexpected has an explicitly deleted default constructor. This was in order to enable the reset idiom exp2 = {} which would otherwise not work due to the ambiguity when deducing the right-hand side argument.

Now that expected<T, E> defaults to T{} the meaning of exp2 = {} is to assign T{}.

3.10. Observers

In order to be as efficient as possible, this proposal includes observers with narrow and wide contracts. Thus, the value() function has a wide contract. If the expected object does not contain a value, an exception is thrown. However, when the user knows that the expected object is valid, the use of operator* would be more appropriated.

3.11. Explicit conversion to bool

The rational described in [N3672] for optional<T> applies to expected<T, E>. The following example therefore combines initialization and value-checking in a boolean context.

if (expected<char, errc> ch = readNextChar()) {
// ...
}

3.12. has_value() following P0032

has_value() has been added to follow [P0032R2].

3.13. Accessing the contained value

Even if expected<T, E> has not been used in practice for enough time as std::optional or Boost.Optional, we consider that following the same interface as std::optional<T> makes the C++ standard library more homogeneous.

The rational described in [N3672] for optional<T> applies to expected<T, E>.

3.14. Dereference operator

The indirection operator was chosen because, along with explicit conversion to bool, it is a very common pattern for accessing a value that might not be there:

if (p) use(*p);

This pattern is used for all sort of pointers (smart or raw) and optional; it clearly indicates the fact that the value may be missing and that we return a reference rather than a value. The indirection operator created some objections because it may incorrectly imply expected and optional are a (possibly smart) pointer, and thus provides shallow copy and comparison semantics. All library components so far use indirection operator to return an object that is not part of the pointer’s/iterator’s value. In contrast, expected as well as optional indirects to the part of its own state. We do not consider it a problem in the design; it is more like an unprecedented usage of indirection operator. We believe that the cost of potential confusion is overweighted by the benefit of an intuitive interface for accessing the contained value.

We do not think that providing an implicit conversion to T would be a good choice. First, it would require different way of checking for the empty state; and second, such implicit conversion is not perfect and still requires other means of accessing the contained value if we want to call a member function on it.

Using the indirection operator for an object that does not contain a value is undefined behavior. This behavior offers maximum runtime performance.

3.15. Function value

In addition to the indirection operator, we propose the member function value as in [N3672] that returns a reference to the contained value if one exists or throw an exception otherwise.

void interact() {
    string s;
    cout << "enter number: ";
    cin >> s;
    expected<int, error> ei = str2int(s);
    try {
        process_int(ei.value());
    }
    catch(bad_expected_access<error>) {
        cout << "this was not a number.";
    }
}

The exception thrown is bad_expected_access<E> (derived from std::exception) which will contain the stored error.

bad_expected_access<E> and bad_optional_access could inherit both from a bad_access exception derived from exception, but this is not proposed yet.

3.16. Should expected<T, E>::value() throw E instead of bad_expected_access<E>?

As any type can be thrown as an exception, should expected<T, E> throw E instead of bad_expected_access<E>?

Some argument that standard function should throw exceptions that inherit from std::exception, but here the exception throw is given by the user via the type E, it is not the standard library that throws explicitly an exception that don’t inherit from std::exception.

This could be convenient as the user will have directly the E exception. However it will be more difficult to identify that this was due to a bad expected access.

If yes, should optional<T> throw nullopt_t instead of bad_optional_access to be coherent?

We don’t propose this.

Other have suggested to throw system_error if E is error_code, rethrow if E is exception_ptr, E if it inherits from std::exception and bad_expected_access<E> otherwise.

An alternative would be to add some customization point that state which exception is thrown but we don’t propose it in this proposal. See the Appendix I.

3.17. Accessing the contained error

Usually, accessing the contained error is done once we know the expected object has no value. This is why the error() function has a narrow contract: it works only if *this does not contain a value.

expected<int, errc> getIntOrZero(istream_range& r) {
    auto r = getInt(); // won’t throw
    if (!r && r.error() == errc::empty_stream) {
        return 0;
    }
    return r;
}

This behavior could not be obtained with the value_or() method since we want to return 0 only if the error is equal to empty_stream.

We could as well provide an error access function with a wide contract. We just need to see how to name each one.

3.18. Conversion to the unexpected value

The error() function is used to propagate errors, as for example in the next example:

expected<pair<int, int>, errc> getIntRange(istream_range& r) {
    auto f = getInt(r);
    if (!f) return unexpected(f.error());
    auto m = matchedString("..", r);
    if (!m) return unexpected(m.error());
    auto l = getInt(r);
    if (!l) return unexpected(l.error());
    return std::make_pair(*f, *l);
}

3.19. Function value_or

The function member value_or() has the same semantics than optional [N3672] since the type of E doesn’t matter; hence we can consider that E == nullopt_t and the optional semantics yields.

This function is a convenience function that should be a non-member function for optional and expected, however as it is already part of the optional interface we propose to have it also for expected.

3.20. Equality operators

As for optional and variant, one of the design goals of expected is that objects of type expected<T, E> should be valid elements in STL containers and usable with STL algorithms (at least if objects of type T and E are). Equality comparison is essential for expected<T, E> to model concept Regular. C++ does not have concepts yet, but being regular is still essential for the type to be effectively used with STL.

3.21. Comparison operators

Comparison operators between expected objects, and between mixed expected and unexpected, aren’t required at this time. A future proposal could re-adopt the comparisons as defined in [P0323R2].

3.22. Modifiers

3.23. Resetting the value

Reseting the value of expected<T, E> is similar to optional<T> but instead of building a disengaged optional<T>, we build an erroneous expected<T, E>. Hence, the semantics and rationale is the same than in [N3672].

3.24. Tag in_place

This proposal makes use of the "in-place" tag as defined in [C++17]. This proposal provides the same kind of "in-place" constructor that forwards (perfectly) the arguments provided to expected's constructor into the constructor of T.

In order to trigger this constructor one has to use the tag in_place. We need the extra tag to disambiguate certain situations, like calling expected's default constructor and requesting T's default construction:

expected<Big, error> eb{in_place, "1"}; // calls Big{"1"} in place (no moving)
expected<Big, error> ec{in_place}; // calls Big{} in place (no moving)
expected<Big, error> ed{}; // calls Big{} (expected state)

3.25. Tag unexpect

This proposal provides an "unexpect" constructor that forwards (perfectly) the arguments provided to expected's constructor into the constructor of E. In order to trigger this constructor one has to use the tag unexpect.

We need the extra tag to disambiguate certain situations, notably if T and E are the same type.

expected<Big, error> eb{unexpect, "1"}; // calls error{"1"} in place (no moving)
expected<Big, error> ec{unexpect}; // calls error{} in place (no moving)

In order to make the tag uniform an additional "expect" constructor could be provided but this proposal doesn’t propose it.

3.26. Requirements on T and E

Class template expected imposes little requirements on T and E: they have to be complete object type satisfying the requirements of Destructible. Each operations on expected<T, E> have different requirements and may be disable if T or E doesn’t respect these requirements. For example, expected<T, E>'s move constructor requires that T and E are MoveConstructible, expected<T, E>'s copy constructor requires that T and E are CopyConstructible, and so on. This is because expected<T, E> is a wrapper for T or E: it should resemble T or E as much as possible. If T and E are EqualityComparable then (and only then) we expect expected<T, E> to be EqualityComparable.

However in order to ensure the never empty guaranties, expected<T, E> requires E to be no throw move constructible. This is normal as the E stands for an error, and throwing while reporting an error is a very bad thing.

3.27. Expected references

This proposal doesn’t include expected references as optional C++17 doesn’t include references either.

We need a future proposal.

3.28. Expected void

While it could seem weird to instantiate optional with void, it has more sense for expected as it conveys in addition, as future<T>, an error state. The type expected<void, E> means "nothing is expected, but an error could occur".

3.29. Making expected a literal type

In [N3672], they propose to make optional a literal type, the same reasoning can be applied to expected. Under some conditions, such that T and E are trivially destructible, and the same described for optional, we propose that expected be a literal type.

3.30. Moved from state

We follow the approach taken in optional [N3672]. Moving expected<T, E> does not modify the state of the source (valued or erroneous) of expected and the move semantics is up to T or E.

3.31. I/O operations

For the same reasons as optional [N3672] we do not add operator<< and operator>> I/O operations.

3.32. What happens when E is a status?

When E is a status, as most of the error codes are, and has more than one value that mean success, setting an expected<T, E> with a successful e value could be misleading if the user expect in this case to have also a T. In this case the user should use the proposed status_value<E, T> class. However, if there is only one value e that mean success, there is no such need and expected<T, E> compose better with the monadic interface [P0650R0].

3.33. Do we need an expected<T, E>::error_or function?

See [P0786R0].

Do we need to add such an error_or function? as member?

This function should work for all the ValueOrError types and so could belong to a future ValueOrError proposal.

Not in this proposal.

3.34. Do we need a expected<T, E>::check_error function?

See [P0786R0].

Do we want to add such a check_error function? as member?

This function should work for all the ValueOrError types and so could belong to a future ValueOrError proposal.

Not in this proposal.

3.35. Do we need a expected<T,G>::adapt_error(function<E(G)) function?

We have the constructor expected<T, E>(expected<T,G>) that allows to transport explicitly the contained error as soon as it is convertible.

However sometimes we cannot change either of the error types and we could need to do this transformation. This function help to achieve this goal. The parameter is the function doing the error transformation.

This function can be defined on top of the existing interface.

template <class T, class E>
expected<T,G> adapt_error(expected<T, E> const& e, function<G(E)> adaptor) {
    if ( !e ) return adaptor(e.error());
    else return expected<T,G>(*e);
}

Do we want to add such a adapt_error function? as member?

This function should work for all the ValueOrError types and so could belong to a future ValueOrError proposal.

Not in this proposal.

4. Wording

Below, substitute the character with a number or name the editor finds appropriate for the sub-section.

4.1. �.� Expected objects [expected]

4.2. �.�.1 In general [expected.general]

This subclause describes class template expected that represents expected objects. An expected<T, E> object holds an object of type T or an object of type unexpected<E> and manages the lifetime of the contained objects.

5. �.�.2 Header <expected> synopsis [expected.synop]

namespace std {

    // �.�.4, class template expected
    template<class T, class E>
        class expected;

    // �.�.5, class template unexpected
    template<class E>
        class unexpected;
    template<class E>
      unexpected(E) -> unexpected<E>;

    // �.�.6, class bad_expected_access
    template<class E>
       class bad_expected_access;

    // �.�.7, Specialization for void
    template<>
       class bad_expected_access<void>;

    // �.�.8, unexpect tag
    struct unexpect_t {
       explicit unexpect_t() = default;
    };
    inline constexpr unexpect_t unexpect{};

}

5.1. �.�.3 Definitions [expected.defs]

5.2. �.�.4 Class template expected [expected.expected]

template<class T, class E>
class expected {
public:
    using value_type = T;
    using error_type = E;
    using unexpected_type = unexpected<E>;

    template<class U>
    using rebind = expected<U, error_type>;

    // �.�.4.1, constructors
    constexpr expected();
    constexpr expected(const expected&);
    constexpr expected(expected&&) noexcept(see below);
    template<class U, class G>
        explicit(see below) constexpr expected(const expected<U, G>&);
    template<class U, class G>
        explicit(see below) constexpr expected(expected<U, G>&&);

    template<class U = T>
        explicit(see below) constexpr expected(U&& v);

    template<class G = E>
        constexpr expected(const unexpected<G>&);
    template<class G = E>
        constexpr expected(unexpected<G>&&);

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


    // �.�.4.2, destructor
    ~expected();

    // �.�.4.3, assignment
    expected& operator=(const expected&);
    expected& operator=(expected&&) noexcept(see below);
    template<class U = T> expected& operator=(U&&);
    template<class G = E>
        expected& operator=(const unexpected<G>&);
    template<class G = E>
        expected& operator=(unexpected<G>&&);

    // �.�.4.4, modifiers

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

    // �.�.4.5, swap
    void swap(expected&) noexcept(see below);

    // �.�.4.6, observers
    constexpr const T* operator->() const;
    constexpr T* operator->();
    constexpr const T& operator*() const&;
    constexpr T& operator*() &;
    constexpr const T&& operator*() const&&;
    constexpr T&& operator*() &&;
    constexpr explicit operator bool() const noexcept;
    constexpr bool has_value() const noexcept;
    constexpr const T& value() const&;
    constexpr T& value() &;
    constexpr const T&& value() const&&;
    constexpr T&& value() &&;
    constexpr const E& error() const&;
    constexpr E& error() &;
    constexpr const E&& error() const&&;
    constexpr E&& error() &&;
    template<class U>
        constexpr T value_or(U&&) const&;
    template<class U>
        constexpr T value_or(U&&) &&;

    // �.�.4.7, Expected equality operators
    template<class T1, class E1, class T2, class E2>
        friend constexpr bool operator==(const expected<T1, E1>& x, const expected<T2, E2>& y);
    template<class T1, class E1, class T2, class E2>
        friend constexpr bool operator!=(const expected<T1, E1>& x, const expected<T2, E2>& y);

    // �.�.4.8, Comparison with T
    template<class T1, class E1, class T2>
        friend constexpr bool operator==(const expected<T1, E1>&, const T2&);
    template<class T1, class E1, class T2>
        friend constexpr bool operator==(const T2&, const expected<T1, E1>&);
    template<class T1, class E1, class T2>
        friend constexpr bool operator!=(const expected<T1, E1>&, const T2&);
    template<class T1, class E1, class T2>
        friend constexpr bool operator!=(const T2&, const expected<T1, E1>&);

    // �.�.4.9, Comparison with unexpected<E>
    template<class T1, class E1, class E2>
        friend constexpr bool operator==(const expected<T1, E1>&, const unexpected<E2>&);
    template<class T1, class E1, class E2>
        friend constexpr bool operator==(const unexpected<E2>&, const expected<T1, E1>&);
    template<class T1, class E1, class E2>
        friend constexpr bool operator!=(const expected<T1, E1>&, const unexpected<E2>&);
    template<class T1, class E1, class E2>
        friend constexpr bool operator!=(const unexpected<E2>&, const expected<T1, E1>&);

    // �.�.4.10, Specialized algorithms
    template<class T1, class E1>
        friend void swap(expected<T1, E1>&, expected<T1, E1>&) noexcept(see below);

private:
    bool has_val; // exposition only
    union
    {
        value_type val; // exposition only
        unexpected_type unex; // exposition only
    };
};

Any object of expected<T, E> either contains a value of type T or a value of type unexpected<E> within its own storage. Implementations are not permitted to use additional storage, such as dynamic memory, to allocate the object of type T or the object of type unexpected<E>. These objects are allocated in a region of the expected<T, E> storage suitably aligned for the types T and unexpected<E>. Members has_val, val and unex are provided for exposition only. has_val indicates whether the expected<T, E> object contains an object of type T.

A program that instantiates the definition of template expected<T, E> for a reference type, a function type, or for possibly cv-qualified types in_place_t, unexpect_t or unexpected<E> for the T parameter is ill-formed. A program that instantiates the definition of template expected<E> with the E parameter that is not a valid template parameter for unexpected is ill-formed.

When T is not cv void, it shall meet the requirements of Cpp17Destructible (Table 27).

E shall meet the requirements of Cpp17Destructible (Table 27).

5.3. �.�.4.1 Constructors [expected.object.ctor]

constexpr expected();

Constraints: is_default_constructible_v<T> is true or T is cv void.

Effects: Value-initializes val if T is not cv void.

Ensures: bool(*this) is true.

Throws: Any exception thrown by the selected constructor of T.

Remarks: If value-initialization of T is a constant subexpression or T is cv void this constructor is constexpr.

constexpr expected(const expected& rhs);

Effects: If bool(rhs) and T is not cv void, initializes val as if direct-non-list-initializing an object of type T with the expression *rhs.

Otherwise, initializes unex as if direct-non-list-initializing an object of type unexpected<E> with the expression unexpected(rhs.error()).

Ensures: bool(rhs) == bool(*this).

Throws: Any exception thrown by the selected constructor of T or E.

Remarks: This constructor is defined as deleted unless:

This constructor is a constexpr constructor if:

constexpr expected(expected&& rhs) noexcept(see below);

Constraints:

Effects: If bool(rhs) and T is not cv void, initializes val as if direct-non-list-initializing an object of type T with the expression std::move(*rhs).

Otherwise, initializes val as if direct-non-list-initializing an object of type unexpected<E> with the expression unexpected(std::move(rhs.error())).

Ensures: bool(rhs) is unchanged, bool(rhs) == bool(*this).

Throws: Any exception thrown by the selected constructor of T or E.

Remarks: The expression inside noexcept is equivalent to:

Remarks: This constructor is a constexpr constructor if:

template<class U, class G>
    explicit(see below) constexpr expected(const expected<U, G>& rhs);

Constraints: T and U are cv void or:

Effects: If bool(rhs) and T is not cv void, initializes val as if direct-non-list-initializing an object of type T with the expression *rhs.

Otherwise, initializes unex as if direct-non-list-initializing an object of type unexpected<E> with the expression unexpected(rhs.error()).

Ensures: bool(rhs) == bool(*this).

Throws: Any exception thrown by the selected constructor of T or E.

Remarks: The expression inside explicit is true if and only if:

template<class U, class G>
    explicit(see below) constexpr expected(expected<U, G>&& rhs);

Constraints: T and U are cv void or:

Effects: If bool(rhs) initializes val as if direct-non-list-initializing an object of type T with the expression std::move(*rhs) or nothing if T is cv void.

Otherwise, initializes unex as if direct-non-list-initializing an object of type unexpected<E> with the expression unexpected(std::move(rhs.error())).

Ensures: bool(rhs) is unchanged, bool(rhs) == bool(*this).

Throws: Any exception thrown by operations specified in the effect clause.

Remarks: The expression inside explicit is true if and only if

template<class U = T>
    explicit(see below) constexpr expected(U&& v);

Constraints:

Effects: Initializes val as if direct-non-list-initializing an object of type T with the expression std::forward<U>(v).

Ensures: bool(*this) is true.

Throws: Any exception thrown by the selected constructor of T.

Remarks: If T's selected constructor is a constant subexpression, this constructor is a constexpr constructor.

Remarks: The expression inside explicit is equivalent to !is_convertible_v<U&&, T>.

template<class G = E>
    explicit(see below) constexpr expected(const unexpected<G>& e);

Constraints: is_constructible_v<E, const G&> is true.

Effects: Initializes unex as if direct-non-list-initializing an object of type unexpected<E> with the expression e.

Ensures: bool(*this) is false.

Throws: Any exception thrown by the selected constructor of E.

Remarks: If unexpected<E>'s selected constructor is a constant subexpression, this constructor shall be a constexpr constructor. The expression inside explicit is equivalent to !is_convertible_v<const G&, E>.

template<class G = E>
    explicit(see below) constexpr expected(unexpected<G>&& e);

Constraints: is_constructible_v<E, G&&> is true.

Effects: Initializes unex as if direct-non-list-initializing an object of type unexpected<E> with the expression std::move(e).

Ensures: bool(*this) is false.

Throws: Any exception thrown by the selected constructor of E.

Remarks: If unexpected<E>'s selected constructor is a constant subexpression, this constructor is a constexpr constructor. The expression inside noexcept is equivalent to: is_nothrow_constructible_v<E, G&&> is true. The expression inside explicit is equivalent to !is_convertible_v<G&&, E>.

template<class... Args>
    constexpr explicit expected(in_place_t, Args&&... args);

Constraints:

Effects: If T is cv void, no effects. Otherwise, initializes val as if direct-non-list-initializing an object of type T with the arguments std::forward<Args>(args)....

Ensures: bool(*this) is true.

Throws: Any exception thrown by the selected constructor of T.

Remarks: If T's constructor selected for the initialization is a constant subexpression, this constructor is a constexpr constructor.

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

Constraints: T is not cv void and is_constructible_v<T, initializer_list<U>&, Args...> is true.

Effects: Initializes val as if direct-non-list-initializing an object of type T with the arguments il, std::forward<Args>(args)....

Ensures: bool(*this) is true.

Throws: Any exception thrown by the selected constructor of T.

Remarks: If T's constructor selected for the initialization is a constant subexpression, this constructor is a constexpr constructor.

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

Constraints: is_constructible_v<E, Args...> is true.

Effects: Initializes unex as if direct-non-list-initializing an object of type unexpected<E>with the arguments std::forward<Args>(args)....

Ensures: bool(*this) is false.

Throws: Any exception thrown by the selected constructor of E.

Remarks: If unexpected<E>'s constructor selected for the initialization is a constant subexpression, this constructor is a constexpr constructor.

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

Constraints: is_constructible_v<E, initializer_list<U>&, Args...> is true.

Effects: Initializes unex as if direct-non-list-initializing an object of type unexpected<E> with the arguments il, std::forward<Args>(args)....

Ensures: bool(*this) is false.

Throws: Any exception thrown by the selected constructor of E.

Remarks: If unexpected<E>'s constructor selected for the initialization is a constant subexpression, this constructor is a constexpr constructor.

5.4. �.�.4.2 Destructor [expected.object.dtor]

~expected();

Effects: If T is not cv void and is_trivially_destructible_v<T> is false and bool(*this), calls val.~T(). If is_trivially_destructible_v<E> is false and !bool(*this), calls unexpect.~unexpected<E>().

Remarks: If either T is cv void or is_trivially_destructible_v<T> is true, and is_trivially_destructible_v<E> is true, then this destructor is a trivial destructor.

5.5. �.�.4.3 Assignment [expected.object.assign]

expected& operator=(const expected& rhs) noexcept(see below);

Effects: See Table editor-please-pick-a-number-0

Table editor-please-pick-a-number-0operator=(const expected&) effects
bool(*this) !bool(*this)
bool(rhs) assigns *rhs to val if T is not cv void
  • if T is cv void destroys unex by calling unexpect.~unexpected<E>() and set has_val to true,

  • otherwise if is_nothrow_copy_constructible_v<T> is true

    • destroys unex by calling unexpect.~unexpected<E>()

    • initializes val as if direct-non-list-initializing an object of type T with *rhs;

  • otherwise if is_nothrow_move_constructible_v<T> is true

    • constructs a T tmp from *rhs (this can throw),

    • destroys unex by calling unexpect.~unexpected<E>()

    • initializes val as if direct-non-list-initializing an object of type T with std::move(tmp);

  • otherwise

    • constructs an unexpected<E> tmp from unexpected(this->error()) (which can throw),

    • destroys unex by calling unexpect.~unexpected<E>(),

    • initializes val as if direct-non-list-initializing an object of type T with *rhs. Either,

    • the constructor didn’t throw, set has_val to true, or

    • the constructor did throw, so move-construct the unexpected<E> from tmp back into the expected storage (which can’t throw as is_nothrow_move_constructible_v<E> is true), and rethrow the exception.

!bool(rhs)
  • if T is cv void

    • initializes unex as if direct-non-list-initializing an object of type unexpected<E> with unexpected(rhs.error()). Either

    • the constructor didn’t throw, set has_val to false, or

    • the constructor did throw, and nothing was changed.

  • otherwise if is_nothrow_copy_constructible_v<E> is true

    • destroys val by calling val.~T(),

    • initializes unex as if direct-non-list-initializing an object of type unexpected<E> with unexpected(rhs.error()) and set has_val to false.

  • otherwise if is_nothrow_move_constructible_v<E> is true

    • constructs an unexpected<E> tmp from unexpected(rhs.error()) (this can throw),

    • destroys val by calling val.~T(),

    • initializes unex as if direct-non-list-initializing an object of type unexpected<E> with std::move(tmp) and set has_val to false.

  • otherwise

    • constructs a T tmp from *this (this can throw),

    • destroys val by calling val.~T(),

    • initializes unex as if direct-non-list-initializing an object of type unexpected<E> with unexpected(rhs.error()). Either,

    • the last constructor didn’t throw, set has_val to false, or

    • the last constructor did throw, so move-construct the T from tmp back into the expected storage (which can’t throw as is_nothrow_move_constructible_v<T> is true), and rethrow the exception.

assigns unexpected(rhs.error()) to unex

Returns: *this.

Ensures: bool(rhs) == bool(*this).

Throws: Any exception thrown by the operations specified in the effect clause.

Remarks: If any exception is thrown, bool(*this) and bool(rhs) remain unchanged.

If an exception is thrown during the call to T's or unexpected<E>'s copy constructor, no effect. If an exception is thrown during the call to T's or unexpected<E>'s copy assignment, the state of its contained value is as defined by the exception safety guarantee of T's or unexpected<E>'s copy assignment.

This operator is defined as deleted unless:

expected& operator=(expected&& rhs) noexcept(see below);

Effects: See Table editor-please-pick-a-number-1

Table editor-please-pick-a-number-1operator=(expected&&) effects
bool(*this) !bool(*this)
bool(rhs) move assign *rhs to val if T is not cv void
  • if T is cv void destroys unex by calling unexpect.~unexpected<E>()

  • otherwise if is_nothrow_move_constructible_v<T> is true

    • destroys unex by calling unexpect.~unexpected<E>(),

    • initializes val as if direct-non-list-initializing an object of type T with *std::move(rhs);

  • otherwise

    • move constructs an unexpected_type<E> tmp from unexpected(this->error()) (which can’t throw as E is nothrow-move-constructible),

    • destroys unex by calling unexpect.~unexpected<E>(),

    • initializes val as if direct-non-list-initializing an object of type T with *std::move(rhs). Either,

    • The constructor didn’t throw, set has_val to true, or

    • The constructor did throw, so move-construct the unexpected<E> from tmp back into the expected storage (which can’t throw as E is nothrow-move-constructible), and rethrow the exception.

!bool(rhs)
  • if T is cv void

    • initializes unex as if direct-non-list-initializing an object of type unexpected<E> with unexpected(move(rhs).error()). Either

    • the constructor didn’t throw, set has_val to false, or

    • the constructor did throw, and nothing was changed.

  • otherwise if is_nothrow_move_constructible_v<E> is true

    • destroys val by calling val.~T(),

    • initializes unex as if direct-non-list-initializing an object of type unexpected<E> with unexpected(std::move(rhs.error()));

  • otherwise

    • move constructs a T tmp from *this (which can’t throw as T is nothrow-move-constructible),

    • destroys val by calling val.~T(),

    • initializes unex as if direct-non-list-initializing an object of type unexpected_type<E> with unexpected(std::move(rhs.error())). Either,

    • The constructor didn’t throw, so mark the expected as holding a unexpected_type<E>, or

    • The constructor did throw, so move-construct the T from tmp back into the expected storage (which can’t throw as T is nothrow-move-constructible), and rethrow the exception.

move assign unexpected(rhs.error()) to unex

Returns: *this.

Ensures: bool(rhs) == bool(*this).

Remarks: The expression inside noexcept is equivalent to: is_nothrow_move_assignable_v<T> is true and is_nothrow_move_constructible_v<T> is true.

If any exception is thrown, bool(*this) and bool(rhs) remain unchanged. If an exception is thrown during the call to T's copy constructor, no effect. If an exception is thrown during the call to T's copy assignment, the state of its contained value is as defined by the exception safety guarantee of T's copy assignment. If an exception is thrown during the call to E's copy assignment, the state of its contained unex is as defined by the exception safety guarantee of E's copy assignment.

This operator is defined as deleted unless:

template<class U = T>
    expected<T, E>& operator=(U&& v);

Constraints:

Effects: See Table editor-please-pick-a-number-2

Table editor-please-pick-a-number-2operator=(U&&) effects
bool(*this) !bool(*this)
If bool(*this), assigns std::forward<U>(v) to val

if is_nothrow_constructible_v<T, U> is true

  • destroys unex by calling unexpect.~unexpected<E>(),

  • initializes val as if direct-non-list-initializing an object of type T with std::forward<U>(v) and

  • set has_val to true;

otherwise

  • move constructs an unexpected<E> tmp from unexpected(this->error()) (which can’t throw as E is nothrow-move-constructible),

  • destroys unex by calling unexpect.~unexpected<E>(),

  • initializes val as if direct-non-list-initializing an object of type T with std::forward<U>(v). Either,

    • the constructor didn’t throw, set has_val to true, that is set has_val to true, or

    • the constructor did throw, so move construct the unexpected<E> from tmp back into the expected storage (which can’t throw as E is nothrow-move-constructible), and re-throw the exception.

Returns: *this.

Ensures: bool(*this) is true.

Remarks: If any exception is thrown, bool(*this) remains unchanged. If an exception is thrown during the call to T's constructor, no effect. If an exception is thrown during the call to T's copy assignment, the state of its contained value is as defined by the exception safety guarantee of T's copy assignment.

template<class G = E>
    expected<T, E>& operator=(const unexpected<G>& e);

Constraints: is_nothrow_copy_constructible_v<E> is true and is_copy_assignable_v<E> is true.

Effects: See Table editor-please-pick-a-number-3

Table editor-please-pick-a-number-3operator=(const unexpected<G>&) effects
bool(*this) !bool(*this)
assigns unexpected(e.error()) to unex
  • destroys val by calling val.~T() if T is not cv void,

  • initializes unex as if direct-non-list-initializing an object of type unexpected<E> with unexpected(e.error()) and set has_val to false.

Returns: *this.

Ensures: bool(*this) is false.

Remarks: If any exception is thrown, bool(*this) remains unchanged.

expected<T, E>& operator=(unexpected<G>&& e);

Effects: See Table editor-please-pick-a-number-4

Table editor-please-pick-a-number-4operator=(unexpected<G>&& e) effects
bool(*this) !bool(*this)
  • destroys val by calling val.~T() if T is not cv void,

  • initializes unex as if direct-non-list-initializing an object of type unexpected<E> with unexpected(std::move(e.error())) and set has_val to false.

move assign unexpected(e.error()) to unex

Constraints: is_nothrow_move_constructible_v<E> is true and is_move_assignable_v<E> is true.

Returns: *this.

Ensures: bool(*this) is false.

Remarks: If any exception is thrown, bool(*this) remains unchanged.

void expected<void, E>::emplace();

Effects:

If !bool(*this)

Ensures: bool(*this) is true.

Throws: Nothing

template<class... Args>
    T& emplace(Args&&... args);

Constraints: T is not cv void and is_nothrow_constructible_v<T, Args...> is true.

Effects:

If bool(*this), assigns val as if constructing an object of type T with the arguments std::forward<Args>(args)...

otherwise if is_nothrow_constructible_v<T, Args...> is true

otherwise if is_nothrow_move_constructible_v<T> is true

otherwise

Ensures: bool(*this) is true.

Returns: A reference to the new contained value val.

Throws: Any exception thrown by the operations specified in the effect clause.

Remarks: If an exception is thrown during the call to T's assignment, nothing changes.

template<class U, class... Args>
    T& emplace(initializer_list<U> il, Args&&... args);

Constraints: T is not cv void and is_nothrow_constructible_v<T, initializer_list<U>&, Args...> is true.

Effects:

If bool(*this), assigns val as if constructing an object of type T with the arguments il, std::forward<Args>(args)...

otherwise if is_nothrow_constructible_v<T, initializer_list<U>&, Args...> is true

otherwise if is_nothrow_move_constructible_v<T> is true

otherwise

Ensures: bool(*this) is true.

Returns: A reference to the new contained value val.

Throws: Any exception thrown by the operations specified in the effect clause.

Remarks: If an exception is thrown during the call to T's assignment nothing changes.

5.6. �.�.4.4 Swap [expected.object.swap]

void swap(expected<T, E>& rhs) noexcept(see below);

Constraints:

Effects: See Table editor-please-pick-a-number-5

Table editor-please-pick-a-number-5swap(expected<T, E>&) effects
bool(*this) !bool(*this)
bool(rhs) if T is not cv void calls using std::swap; swap(val, rhs.val) calls rhs.swap(*this)
!bool(rhs)
  • if T is cv void

    • initializes unex as if direct-non-list-initializing an object of type unexpected<E> with unexpected(std::move(rhs)). Either

    • the constructor didn’t throw, set has_val to false, destroys rhs.unexpect by calling rhs.unexpect.~unexpected<E>() set rhs.has_val to true.

    • the constructor did throw, rethrow the exception.

  • otherwise if is_nothrow_move_constructible_v<E> is true,

    • the unex of rhs is moved to a temporary variable tmp of type unexpected_type,

    • followed by destruction of unex as if by rhs.unexpect.~unexpected<E>(),

    • rhs.val is direct-initialized from std::move(*this). Either

      • the constructor didn’t throw

        • destroy val as if by this->val.~T(),

        • the unex of this is direct-initialized from std::move(tmp), after this, this does not contain a value; and bool(rhs).

      • the constructor did throw, so move-construct the unexpected<E> from tmp back into the expected storage (which can’t throw as E is nothrow-move-constructible), and re-throw the exception.

  • otherwise if is_nothrow_move_constructible_v<T> is true,

    • val is moved to a temporary variable tmp of type T,

    • followed by destruction of val as if by val.~T(),

    • the unex is direct-initialized from unexpected(std::move(other.error())). Either

      • the constructor didn’t throw

        • destroy rhs.unexpect as if by rhs.unexpect.~unexpected<E>(),

        • rhs.val is direct-initialized from std::move(tmp), after this, this does not contain a value; and bool(rhs).

      • the constructor did throw, so move-construct the T from tmp back into the expected storage (which can’t throw as T is nothrow-move-constructible), and re-throw the exception.

  • otherwise, the function is not defined.

calls using std::swap; swap(unexpect, rhs.unexpect)

Throws: Any exceptions that the expressions in the Effects clause throw.

Remarks: The expression inside noexcept is equivalent to: is_nothrow_move_constructible_v<T> is true and is_nothrow_swappable_v<T> is true and is_nothrow_move_constructible_v<E> is true and is_nothrow_swappable_v<E> is true.

5.7. �.�.4.5 Observers [expected.object.observe]

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

Constraints: T is not cv void.

Expects: bool(*this) is true.

Returns: addressof(val).

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

Constraints: T is not cv void.

Expects: bool(*this) is true.

Returns: val.

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

Constraints: T is not cv void.

Expects: bool(*this) is true.

Returns: std::move(val).

constexpr explicit operator bool() noexcept;

Returns: has_val.

constexpr bool has_value() const noexcept;

Returns: has_val.

constexpr void expected<void, E>::value() const;

Throws: bad_expected_access(error()) if !bool(*this).

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

Constraints: T is not cv void.

Returns: val, if bool(*this) .

Throws: bad_expected_access(error()) if !bool(*this).

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

Constraints: T is not cv void.

Returns: std::move(val), if bool(*this) .

Throws: bad_expected_access(error()) if !bool(*this).

constexpr const E& error() const&;
constexpr E& error() &;

Expects: bool(*this) is false.

Returns: unexpect.value().

constexpr E&& error() &&;
constexpr const E&& error() const&&;

Expects: bool(*this) is false.

Returns: std::move(unexpect.value()).

template<class U>
    constexpr T value_or(U&& v) const&;

Returns: bool(*this) ? **this : static_cast<T>(std::forward<U>(v));.

Remarks: If is_copy_constructible_v<T> is trueand is_convertible_v<U, T> is false the program is ill-formed.

template<class U>
    constexpr T value_or(U&& v) &&;

Returns: bool(*this) ? std::move(**this) : static_cast<T>(std::forward<U>(v));.

Remarks: If is_move_constructible_v<T> is true and is_convertible_v<U, T> is false the program is ill-formed.

5.8. �.�.4.6 Expected Equality operators [expected.equality_op]

template<class T1, class E1, class T2, class E2>
    friend constexpr bool operator==(const expected<T1, E1>& x, const expected<T2, E2>& y);

Expects: The expressions *x == *y and x.error())== y.error() are well-formed and their results are convertible to bool.

Returns: If bool(x) != bool(y), false; otherwise if bool(x) == false, x.error() == y.error(); otherwise true if T1 and T2 are cv void or *x == *y otherwise.

Remarks: Specializations of this function template, for which T1 and T2 are cv void or *x == *y and x.error() == y.error() is a core constant expression, are constexpr functions.

template<class T1, class E1, class T2, class E2>
    constexpr bool operator!=(const expected<T1, E1>& x, const expected<T2, E2>& y);

Mandates: The expressions *x != *y and x.error() != y.error() are well-formed and their results are convertible to bool.

Returns: If bool(x) != bool(y), true; otherwise if bool(x) == false, x.error() != y.error(); otherwise true if T1 and T2 are cv void or *x != *y.

Remarks: Specializations of this function template, for which T1 and T2 are cv void or *x != *y and x.error() != y.error() is a core constant expression, are constexpr functions.

5.9. �.�.4.7 Comparison with T [expected.comparison_T]

template<class T1, class E1, class T2> constexpr bool operator==(const expected<T1, E1>& x, const T2& v);
template<class T1, class E1, class T2> constexpr bool operator==(const T2& v, const expected<T1, E1>& x);

Mandates: T1 and T2 are not cv void and the expression *x == v is well-formed and its result is convertible to bool. [ Note: T1 need not be EqualityComparable. - end note]

Returns: bool(x) ? *x == v : false;.

template<class T1, class E1, class T2> constexpr bool operator!=(const expected<T1, E1>& x, const T2& v);
template<class T1, class E1, class T2> constexpr bool operator!=(const T2& v, const expected<T1, E1>& x);

Mandates: T1 and T2 are not cv void and the expression *x == v is well-formed and its result is convertible to bool. [ Note: T1 need not be EqualityComparable. - end note]

Returns: bool(x) ? *x != v : false;.

5.10. �.�.4.8 Comparison with unexpected<E> [expected.comparison_unexpected_E]

template<class T1, class E1, class E2> constexpr bool operator==(const expected<T1, E1>& x, const unexpected<E2>& e);
template<class T1, class E1, class E2> constexpr bool operator==(const unexpected<E2>& e, const expected<T1, E1>& x);

Mandates: The expression unexpected(x.error()) == e is well-formed and its result is convertible to bool.

Returns: bool(x) ? false : unexpected(x.error()) == e;.

template<class T1, class E1, class E2> constexpr bool operator!=(const expected<T1, E1>& x, const unexpected<E2>& e);
template<class T1, class E1, class E2> constexpr bool operator!=(const unexpected<E2>& e, const expected<T1, E1>& x);

Mandates: The expression unexpected(x.error()) != e is well-formed and its result is convertible to bool.

Returns: bool(x) ? true : unexpected(x.error()) != e;.

5.11. �.�.4.9 Specialized algorithms [expected.specalg]

template<class T1, class E1>
void swap(expected<T1, E1>& x, expected<T1, E1>& y) noexcept(noexcept(x.swap(y)));

Constraints:

Effects: Calls x.swap(y).

5.12. �.�.5 Unexpected objects [expected.unexpected]

5.13. �.�.5.1 General [expected.unexpected.general]

This subclause describes class template unexpected that represents unexpected objects.

5.14. �.�.5.2 Class template unexpected [expected.unexpected.object]

template<class E>
class unexpected {
public:
    constexpr unexpected(const unexpected&) = default;
    constexpr unexpected(unexpected&&) = default;
    template<class... Args>
        constexpr explicit unexpected(in_place_t, Args&&...);
    template<class U, class... Args>
        constexpr explicit unexpected(in_place_t, initializer_list<U>, Args&&...);
    template<class Err = E>
        constexpr explicit unexpected(Err&&);
    template<class Err>
        constexpr explicit(see below) unexpected(const unexpected<Err>&);
    template<class Err>
        constexpr explicit(see below) unexpected(unexpected<Err>&&);

    constexpr unexpected& operator=(const unexpected&) = default;
    constexpr unexpected& operator=(unexpected&&) = default;
    template<class Err = E>
    constexpr unexpected& operator=(const unexpected<Err>&);
    template<class Err = E>
    constexpr unexpected& operator=(unexpected<Err>&&);

    constexpr const E& value() const& noexcept;
    constexpr E& value() & noexcept;
    constexpr const E&& value() const&& noexcept;
    constexpr E&& value() && noexcept;

    void swap(unexpected& other) noexcept(see below);

    template<class E1, class E2>
        friend constexpr bool
        operator==(const unexpected<E1>&, const unexpected<E2>&);
    template<class E1, class E2>
        constexpr bool
        operator!=(const unexpected<E1>&, const unexpected<E2>&);

    template<class E1>
    friend void swap(unexpected<E1>& x, unexpected<E1>& y) noexcept(noexcept(x.swap(y)));

private:
    E val; // exposition only
};

template<class E> unexpected(E) -> unexpected<E>;

A program that instantiates the definition of unexpected for a non-object type, an array type, a specialization of unexpected or an cv-qualified type is ill-formed.

5.14.1. �.�.5.2.1 Constructors [expected.unexpected.ctor]

template<class Err>
    constexpr explicit unexpected(Err&& e);

Constraints:

Effects: Initializes val as if direct-non-list-initializing an object of type E with the expression std::forward<Err>(e).

Throws: Any exception thrown by the selected constructor of E.

Remarks: If E’s selected constructor is a constant subexpression, this constructor is a constexpr constructor.

template<class... Args>
    constexpr explicit unexpected(in_place_t, Args&&...);

Constraints: is_constructible_v<E, Args...> is true.

Effects: Initializes val as if direct-non-list-initializing an object of type E with the arguments std::forward<Args>(args)....

Throws: Any exception thrown by the selected constructor of E.

Remarks: If E’s selected constructor is a constant subexpression, this constructor is a constexpr constructor.

template<class U, class... Args>
    constexpr explicit unexpected(in_place_t, initializer_list<U>, Args&&...);

Constraints: is_constructible_v<E, initializer_list<U>&, Args...> is true.

Effects: Initializes val as if direct-non-list-initializing an object of type E with the arguments il, std::forward<Args>(args)....

Throws: Any exception thrown by the selected constructor of E.

Remarks: If E’s selected constructor is a constant subexpression, this constructor is a constexpr constructor.

template<class Err>
constexpr explicit(see below) unexpected(const unexpected<Err>& e);

Constraints:

Effects: Initializes val as if direct-non-list-initializing an object of type E with the expression e.val.

Throws: Any exception thrown by the selected constructor of E.

Remarks: The expression inside explicit is equivalent to !is_convertible_v<const Err&, E>.

template<class Err>
constexpr explicit(see below) unexpected(unexpected<Err>&& e);

Constraints:

Effects: Initializes val as if direct-non-list-initializing an object of type E with the expression std::move(e.val).

Throws: Any exception thrown by the selected constructor of E.

Remarks: The expression inside explicit is equivalent to !is_convertible_v<Err, E>.

5.14.2. �.�.5.2.2 Assignment [expected.unexpected.assign]

template<class Err = E>
    constexpr unexpected& operator=(const unexpected<Err>& e);

Constraints: is_assignable_v<E, const Err&> is true.

Effects: Equivalent to val = e.val.

Returns: *this.

Remarks: If E’s selected assignment operator is a constant subexpression, this function is a constexpr function.

template<class Err = E>
    constexpr unexpected& operator=(unexpected<Err>&& e);

Constraints: is_assignable_v<E, Err> is true.

Effects: Equivalent to val = std::move(e.val).

Returns: *this.

Remarks: If E’s selected assignment operator is a constant subexpression, this function is a constexpr function.

5.14.3. �.�.5.2.3 Observers [expected.unexpected.observe]

constexpr const E& value() const&;
constexpr E& value() &;

Returns: val.

constexpr E&& value() &&;
constexpr const E&& value() const&&;

Returns: std::move(val).

5.14.4. �.�.5.2.4 Swap [expected.unexpected.ctor]

void swap(unexpected& other) noexcept(see below);

Mandates: is_swappable_v<E> is true.

Effects: Equivalent to using std::swap; swap(val, other.val);.

Throws: Any exceptions thrown by the operations in the relevant part of [expected.swap].

Remarks: The expression inside noexcept is equivalent to: is_nothrow_swappable_v<E> is true.

5.15. �.�.5.2.5 Equality operators [expected.unexpected.equality_op]

template<class E1, class E2>
    friend constexpr bool operator==(const unexpected<E1>& x, const unexpected<E2>& y);

Returns: x.value() == y.value().

template<class E1, class E2>
    friend constexpr bool operator!=(const unexpected<E1>& x, const unexpected<E2>& y);

Returns: x.value() != y.value().

5.15.1. �.�.5.2.5 Specialized algorithms [expected.unexpected.specalg]

template<class E1>
    friend void swap(unexpected<E1>& x, unexpected<E1>& y) noexcept(noexcept(x.swap(y)));

Constraints: is_swappable_v<E1> is true.

Effects: Equivalent to x.swap(y).

5.16. �.�.6 Template Class bad_expected_access [expected.bad_expected_access]

template<class E>
class bad_expected_access : public bad_expected_access<void> {
public:
    explicit bad_expected_access(E);
    virtual const char* what() const noexcept override;
    E& error() &;
    const E& error() const&;
    E&& error() &&;
    const E&&  error() const&&;
private:
    E val; // exposition only
};

The template class bad_expected_access defines the type of objects thrown as exceptions to report the situation where an attempt is made to access the value of expected object that contains an unexpected<E>.

bad_expected_access::bad_expected_access(E e);

Effects: Initializes val with e.

Ensures: what() returns an implementation-defined NTBS.

const E& error() const&;
E& error() &;

Returns: val;

E&& error() &&;
const E&& error() const&&;

Returns: std::move(val);

virtual const char* what() const noexcept override;

Returns: An implementation-defined NTBS.

5.17. �.�.7 Class bad_expected_access<void> [expected.bad_expected_access_base]

template<>
class bad_expected_access<void> : public exception {
public:
    explicit bad_expected_access();
};

5.18. �.�.8 unexpect tag [expected.unexpect]

struct unexpect_t(see below);
inline constexpr unexpect_t unexpect(unspecified);

Type unexpect_t does not have a default constructor or an initializer-list constructor, and is not an aggregate.

5.19. Feature test macro

The proposed library feature test macro is __cpp_lib_expected.

6. Implementation & Usage Experience

There are multiple implementations of std::expected as specified in this paper, listed below. There are also many implementations which are similar but not the same as specified in this paper, they are not listed below.

6.1. Sy Brand

By far the most popular implementation is Sy Brand’s, with over 500 stars on GitHub and extensive usage.

6.2. Vicente J. Botet Escriba

The original author of std::expected has an implementation available:

6.3. WebKit

The WebKit web browser (used in Safari) contains an implementation that’s used throughout its codebase.

References

Informative References

[N3527]
F. Cacciola, A. Krzemieński. A proposal to add a utility class to represent optional objects (Revision 2). 10 March 2013. URL: https://wg21.link/n3527
[N3672]
F. Cacciola, A. Krzemieński. A proposal to add a utility class to represent optional objects (Revision 4). 19 April 2013. URL: https://wg21.link/n3672
[N3793]
F. Cacciola, A. Krzemieński. A proposal to add a utility class to represent optional objects (Revision 5). 3 October 2013. URL: https://wg21.link/n3793
[N4015]
V. Escriba, P. Talbot. A proposal to add a utility class to represent expected monad. 26 May 2014. URL: https://wg21.link/n4015
[N4109]
V. Escriba, P. Talbot. A proposal to add a utility class to represent expected monad - Revision 1. 29 June 2014. URL: https://wg21.link/n4109
[P0032R2]
Vicente J. Botet Escriba. Homogeneous interface for variant, any and optional (Revision 2). 13 March 2016. URL: https://wg21.link/p0032r2
[P0110R0]
Anthony Williams. Implementing the strong guarantee for variant<> assignment. 25 September 2015. URL: https://wg21.link/p0110r0
[P0157R0]
Lawrence Crowl. Handling Disappointment in C++. 7 November 2015. URL: https://wg21.link/p0157r0
[P0262r1]
Lawrence Crowl, Chris Mysen. A Class for Status and Optional Value. 15 October 2016. URL: https://wg21.link/p0262r1
[P0323R2]
Vicente J. Botet Escriba. A proposal to add a utility class to represent expected object (Revision 4). 15 June 2017. URL: https://wg21.link/p0323r2
[P0323r3]
Vicente J. Botet Escriba. Utility class to represent expected object. 15 October 2017. URL: https://wg21.link/p0323r3
[P0323r9]
JF Bastien, Vicente Botet. std::expected. 3 August 2019. URL: https://wg21.link/p0323r9
[P0343r1]
Vicente J. Botet Escriba. Meta-programming High-Order Functions. 15 June 2017. URL: https://wg21.link/p0343r1
[P0650R0]
Vicente J. Botet Escriba. C++ Monadic interface. 15 June 2017. URL: https://wg21.link/p0650r0
[P0650r2]
Vicente J. Botet Escribá. C++ Monadic interface. 11 February 2018. URL: https://wg21.link/p0650r2
[P0709r4]
Herb Sutter. Zero-overhead deterministic exceptions: Throwing values. 4 August 2019. URL: https://wg21.link/p0709r4
[P0762r0]
Niall Douglas. Concerns about expected<T, E> from the Boost.Outcome peer review. 15 October 2017. URL: https://wg21.link/p0762r0
[P0786R0]
Vicente J. Botet Escriba. SuccessOrFailure, ValuedOrError and ValuedOrNone types. 15 October 2017. URL: https://wg21.link/p0786r0
[P1028R3]
Niall Douglas. SG14 status_code and standard error object. 12 January 2020. URL: https://wg21.link/p1028r3
[P1051r0]
Vicente J. Botet Escribá. std::experimental::expected LWG design issues. 3 May 2018. URL: https://wg21.link/p1051r0
[P1095r0]
Niall Douglas. Zero overhead deterministic failure - A unified mechanism for C and C++. 29 August 2018. URL: https://wg21.link/p1095r0