[P0323R11] (will be update to R12 once that revision published) introduced class template
,
which is a vocabulary type that contains an expected value of type
or an error
.
Similar to
,
provided member function
and
in order to allow access to the contained value or error type.
The proposal also includes the auxillary type
to wrap the error type, to both disambiguating between value and error types, and also introduce
explicit marker for returning error (unexpected outcome) types. The introduction of the wrapper type allows both
and
to be
implicitly convertible to
, and thus allows the following usage:
However, even thoughstd :: expected < int , std :: errc > svtoi ( std :: string_view sv ) { int value { 0 }; auto [ ptr , ec ] = std :: from_chars ( sv . begin (), sv . end (), value ); if ( ec == std :: errc {}) { return value ; } return std :: unexpected ( ec ); }
std :: unexpected < E >
is simply a wrapper over E
, we need to use its member method value ()
to access the contained error value.
The name of the member method is inconsistent with the std :: expected < T , E >
usage and intuition, so this proposal seeks to correct the name of the member access method to error ()
.
[P0323R11] is adopted for C++23 at the Feburary 2022 WG21 Plenary, so this proposal also targets C++23 to fix this.
1. Revision History
1.1. R0
Initial revision.2. Motivation
Consistency among library vocabulary types is important and makes user interaction intuitive, and since
is specifically based on and extends
[N3793], it is especially important to maintain a similar interface between
and
, and also within the
design.
By this way, users will not be surprised if they switch between different sum types.
We can have a comparison on the various member access method of the
and
interface:
Member | Return Type |
|
|
|
|
(Normal) Value |
|
|
| N/A | N/A |
Unexpected Outcome (Error) | (
)
| N/A |
|
|
|
We can see that the only outlier in this table is
, which is both inconsistent with
and
that also (possibly) holds an error value, and also inconsistent with other standard library types providing
, including
and
.
These types all provide
to access the normal value they hold, and often have preconditions (or throw exceptions if violated) that they holds a value instead of an error.
Provide
instead of
for
has several benefits:
-
Consistency: Both consistent with
andstd :: expected < T , E >
to providestd :: bad_expected_access < E >
to return error value, and also reduce inconsistency with othererror ()
-providing types that have different preconditions.value () -
Generic: Same name means more generic code is allowed. For example, generic code can do
on any (potentially) error-wrapping types to retrieve the error, this includese . error ()
,std :: expected < T , E >
,std :: unexpected < E >
, and possible further error-handling types likestd :: bad_expected_access < E >
[P1028R3] andstd :: status_code
[P0709R4].std :: error -
Safety & Inituitive: Other
type often have different preconditions, for example throwing when the type does not holds a normal value, or (worse) have narrow contract and UB on abnormal call. Passing the currentvalue ()
-wrapped type to interface expecting the normalstd :: unexpected < E >
semantics can be surprising when leading to runtime exception or (worse) UB.value ()
Side note: you can even smell the inconsistency when many of the wording of equality operator between
and
in [P0323R11] contains clause such as
.
3. Design
3.1. Alternative Design
This section lists the alternative choices and possible arguments against this proposal that has been considered.3.1.1. But we are already badly inconsistent!
Some may argue that the intensive use ofvalue ()
across the library is already inconsistent, and we do not need to keep it consistent.
I would argue that most use of
member function across the standard library adhere to the tradition, aka return the normal "value" that the type is holding.
Functions like
can be thought as an extended definition of "value": leap second can hold either
or
as value.
The only case that I agree are related is the C++11
/
pair and their
member that returns the error code,
which seems to return an error(-related) value. However, I want to point out that
is not really the "error value" or "unexpected outcome" of these types,
since this is the expected outcome (or "normal value") or an
. Furthermore,
is not really the whole "error" contained in these types,
since these two types consists of
plus
. Only
cannot represent a unique error, and should not be taken as the "error representation".
3.1.2. Conversion operator
The other standard library wrapper type,std :: reference_wrapper < T >
, provided an (implicit) conversion operator to T &
, its wrapped value. This leads to thoughts on
whether std :: unexpected < E >
should simply provide an (implicit or explicit) conversion operator to E
as its member access method.
A similar choice had been facing the designer of
, and their decision (later inherited by
) is to reject: ([N3672], 7.9)
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.
I think that this reasoning also applies here. Even if it is implicit, conversion operator is not perfect (call member functions), and we still need
or other member accessors to do that. Also, there seems to be no benefit in providing such conversion (besides,
is just intended as a "trampoline"
for constructing an
, it is not intended to be used extensively/on its own). Therefore I rejected this option.
3.1.3. No member accessor
The above discussion leads to the consideration: sincestd :: unexpected < E >
is just meant as a "trampoline", does it need a member accessor at all?
Besides, the intended usage is just return std :: unexpected ( some_error );
, provide a member accessor does not seems to help this use case at all.
This is an interesting point. Also, one of [P0323R11]'s referenced implementation [viboes-expected] does this: its
type
have no accessor at all. However, providing an accessor does not seems to do any harm, and may have interesting use cases that I’m not aware of. Therefore
I do not propose this, but will not against changing the proposal to this direction if L(E)WG favor this.
3.2. Target Vehicle
This proposal targets C++23. I’m aware that the design freeze deadline of C++23 is already passed, but I think this can be classified as an improvement/fix over the defect instd :: expected < T , E >
. Furthermore, this proposal will be a huge breaking change (that makes it simply unviable to propose) after C++23.
3.3. Feature Test Macro
As long as the proposal lands in C++23, I don’t think there is a need to change any feature test macro. However, if L(E)WG feels there is a need, then I suggest bumping__cpp_lib_expected
to the date of adoption (definitely larger than 202202L
).
4. Implementation & Usage Experience
The referenced implementation of [P0323R11] all implement the interface of original proposal (except [viboes-expected] mentioned above). This section thus investigated several similar implementations.
4.1. Outcome v2
[outcome-v2] is a popular library invested in a set of tools for reporting and handling function failures in contexts where directly using C++ exception handling is unsuitable. It is both provided as Boost.Outcome and the standalone GitHub repository, and also having an experimental branch that is the basis of [P1095R0] and [P1028R3]std :: status_code
. The library provided result < T , E , Policy >
and outcome < T , EC , EP , Policy >
types that represents value/error
duo type, just like std :: expected < T , E >
, with the difference in interface and also outcome
can hold both EC
and EP
(error code and exception (pointer)).
The design of result < T , E >
also deeply influences [P0323R11], and the final adopted design of std :: expected < T , E >
is very similar to what outcome :: result < T , E >
provides.
One of the main design difference is that
can be implicitly constructed from both
and
, while
can only be implicitly constructed from the former.
For this reason,
does not allow for
and
to be the same, and also does not provide
and
accessor.
Thus, there are wrapper for both success and failure value for construction, and
wrap a success
, while
wraps an
unexpected
(or
and
). Their accessors are: (the
narrow-contract accessors and
are not shown)
Member | Return Type |
|
|
|
|
(Normal) Value |
|
|
|
| N/A |
Unexpected Outcome (Error) | (or and )
|
|
| N/A |
|
We can see that Outcome v2 is pretty consistent in member accessor, and especially its
provides
and
,
not
. Also note that the default exception being thrown,
and
, does not holds the error/exception value at all.
There is a
for consistency with
.
4.2. Boost.LEAF
Lightweight Error Augmentation Framework (LEAF), or [Boost.LEAF], is a lightweight error handling library for C++11. It is intended to be an improved version of Outcome, by eliminating branchy code and remove error type fromresult < T , E >
signature. The author describe it as
LEAF is designed with a strong bias towards the common use case where callers of functions which may fail check for success and forward errors up the call stack but do not handle them. In this case, only a trivial success-or-failure discriminant is transported. Actual error objects are communicated directly to the error handling scope, skipping the intermediate check-only frames altogether.
The main type for LEAF is
, which is again a counterpart of
and
, but with
eliminated from signature.
Unexpected results are produced by
, which returns an
object that can be converted to an unexpected
.
There is also a
that is used as the generic error type receiver for functions such as
.
The member accessor is:
Member | Return Type |
|
|
(Normal) Value |
|
| N/A |
Unexpected Outcome (Error) |
|
|
|
leaf :: error_id
is the final error (unexpected outcome) type, its value ()
is similar to that of std :: error_code
, which does not return an "unexpected outcome",
but instead return an error ID for the alternative description of leaf :: error_id
, which actually fits into my reasoning of returning "value".) Again we
can see a consistency here.
5. Wording
The wording below is based on [N4901] plus the editorial PR of P0323R12. This will be rebased onto post-Feburary Working Draft in the next revision, with hopefully no change of section name.
5.1. 20.9.3.2 Class template unexpected
[expected.un.object]
5.1.1. 20.9.3.2.1 General [expected.un.object.general]
namespace std { 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 && ); constexpr unexpected & operator = ( const unexpected & ) = default ; constexpr unexpected & operator = ( unexpected && ) = default ; constexpr const E & value error () const & noexcept ; constexpr E & value error () & noexcept ; constexpr const E && value error () const && noexcept ; constexpr E && value error () && noexcept ; constexpr void swap ( unexpected & other ) noexcept ( see below ); template < class E2 > friend constexpr bool operator == ( const unexpected & , const unexpected < E2 >& ); friend constexpr void swap ( unexpected & x , unexpected & y ) noexcept ( noexcept ( x . swap ( y ))); private : E val ; // exposition only }; template < class E > unexpected ( E ) -> unexpected < E > ; }
5.1.2. 20.9.3.2.3 Observers [expected.un.obs]
constexpr const E & value error () const & noexcept ; constexpr E & value error () & noexcept ;
Returns:
.
constexpr E && value error () && noexcept ; constexpr const E && value error () const && noexcept ;
Returns:
.
5.1.3. 20.9.3.2.5 Equality operator [expected.un.eq]
template < class E2 > friend constexpr bool operator == ( const unexpected & x , const unexpected < E2 >& y );
Mandates: The expression
is well-formed and its result
is convertible to
.
Returns:
.
5.2. 20.9.6 Class template expected
[expected.expected]
5.2.1. 20.9.6.2 Constructors [expected.object.ctor]
template < class G > constexpr explicit ( ! is_convertible_v < const G & , E > ) expected ( const unexpected < G >& e ); template < class G > constexpr explicit ( ! is_convertible_v < G , E > ) expected ( unexpected < G >&& e );
Let
be
for the first overload and
for the second overload.
Constraints:
is true
.
Effects: Direct-non-list-initializes
with
.
Postconditions:
is false
.
Throws: Any exception thrown by the initialization of
.
5.2.2. 20.9.6.4 Assignment [expected.object.assign]
template < class G > constexpr expected & operator = ( const unexpected < G >& e ); template < class G > constexpr expected & operator = ( unexpected < G >&& e );
Let
be
for the first overload and
for the second overload.
Constraints:
-
isis_constructible_v < E , GF > true
; and -
isis_assignable_v < E & , GF > true
; and -
isis_nothrow_constructible_v < E , GF > || is_nothrow_move_constructible_v < T > || is_nothrow_move_constructible_v < E > true
.
Effects:
-
If
ishas_value () true
, equivalent to:reinit - expected ( unex , val , std :: forward < GF > ( e . value error ())); has_val = false; -
Otherwise, equivalent to:
unex = std :: forward < GF > ( e . value error ());
Returns:
.
5.2.3. 20.9.6.7 Equality operators [expected.object.eq]
template < class E2 > friend constexpr bool operator == ( const expected & x , const unexpected < E2 >& e );
Mandates: The expression
is well-formed and
its result is convertible to
.
Returns:
.
5.3. 20.9.7 Partial specialization of expected
for void
types [expected.void]
5.3.1. 20.9.7.2 Constructors [expected.void.ctor]
template < class G > constexpr explicit ( ! is_convertible_v < const G & , E > ) expected ( const unexpected < G >& e ); template < class G > constexpr explicit ( ! is_convertible_v < G , E > ) expected ( unexpected < G >&& e );
Let
be
for the first overload and
for the second overload.
Constraints:
is true
.
Effects: Direct-non-list-initializes
with
.
Postconditions:
is false
.
Throws: Any exception thrown by the initialization of
.
5.3.2. 20.9.7.4 Assignment [expected.void.assign]
template < class G > constexpr expected & operator = ( const unexpected < G >& e ); template < class G > constexpr expected & operator = ( unexpected < G >&& e );
Let
be
for the first overload and
for the second overload.
Constraints:
is true
and
is true
.
Effects:
-
If
ishas_value () true
, equivalent to:construct_at ( addressof ( unex ), std :: forward < GF > ( e . value error ())); has_val = false; -
Otherwise, equivalent to:
unex = std :: forward < GF > ( e . value error ());
Returns:
.
5.3.3. 20.9.7.7 Equality operators [expected.void.eq]
template < class E2 > constexpr bool operator == ( const expected & x , const unexpected < E2 >& e );
Mandates: The expression
is well-formed and
its result is convertible to
.
Returns:
.