[P0323R12] introduced class template
,
a vocabulary type containing 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 auxiliary 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 ()
.
[P0323R12] is adopted for C++23 at February 2022 WG21 Plenary, so this proposal also targets C++23 to fix this.
1. Revision History
1.1. R1
-
Rebased onto [P0323R12] and [N4910].
-
Fixed some typos and added examples.
-
LEWG sees [P2549R0] at the 2022-03-01 Telecon, with the following poll:
Poll: Advance [P2549R0] to electronic polling to send it to LWG for C++23 (as a [P0592R4] priority 2 item)
SF | F | N | A | SA |
7 | 5 | 2 | 0 | 0 |
Outcome: Strong Consensus in Favor 🎉
In the 2022-05 Library Evolution Electronic Polling period, the paper was forwarded to LWG.
Poll 1.12: Send [P2549R0] to Library Working Group for C++23, classified as an improvement of an existing feature ([P0592R4] bucket 2 item).
SF | F | N | A | SA |
9 | 9 | 2 | 0 | 0 |
Outcome: Strong Consensus in Favor 🎉
This revision thus targets LWG.
-
Since [P2505R2] seems to target C++23, add some discussion regarding that paper.
-
Address feedback on R0: Incorporate wording to also rename the exposition only private member of
andstd :: unexpected < E >
fromstd :: bad_expected_access < E >
toval
, to be consistent withunex
.std :: expected < T , E >
1.2. R0
-
Initial revision.
2. Motivation
Consistency among library vocabulary types is essential and makes user interaction intuitive. Since
is specifically based on and extends
[N3793], it is especially important to maintain a similar interface between
and
, and also within the
design.
In 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 hold 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 & Intuitive: Other
types often has different preconditions, for example, throwing when the type does not hold a normal value or (worse) have a 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 ()
Before change:
After change:void fun () { using namespace std :: literals ; using ET = std :: expected < int , std :: string > ; auto unex = std :: unexpected ( "Oops" s ); auto wrapped = unex . value (); // okay, get "Oops" auto ex = ET ( unex ); // implicit, can also happen in parameter passing, etc. auto wrapped2 = ex . value (); // throws! }
void fun () { using namespace std :: literals ; using ET = std :: expected < int , std :: string > ; auto unex = std :: unexpected ( "Oops" s ); auto wrapped = unex . error (); // okay, get "Oops" auto ex = ET ( unex ); // implicit, can also happen in parameter passing, etc. auto wrapped2 = ex . error (); // okay, get "Oops" too. }
Side note: you can even smell the inconsistency when many of the wording of equality operator between
and
in [P0323R12] contains clause such as
.
3. Design
3.1. Alternative Design
This section lists the alternative choices and possible arguments against this proposal that have 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 adheres 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") on a
. Furthermore,
is not really the whole "error" contained in these types
since these two types consist 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, the 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 a
, 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 );
, providing a member accessor does not seem to help this use case at all.
This is an interesting point. Also, one of [P0323R12]'s referenced implementation [viboes-expected] does this: its
type
have no accessor at all. However, providing an accessor does not seem 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 be against changing the proposal to this direction if L(E)WG favors 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 or if [P2505R2] ends up bumping the feature test macro, then I suggest bumping__cpp_lib_expected
to the date of adoption (definitely larger than 202202L
, and probably share a value with [P2505R2]).
4. Implementation & Usage Experience
The referenced implementation of [P0323R12] all implement the interface of the 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 has 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 the outcome
can hold both EC
and EP
(error code and exception (pointer)).
The design of result < T , E >
also deeply influences [P0323R12], 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 differences 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 wrappers 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 hold 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 removing error type fromresult < T , E >
signature. The author describes 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 the signature.
Unexpected results are produced by
, which returns a
object that the user can convert 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 consistency here.
5. Wording
The wording below is based on [N4910]. Wording includes only the renaming of
to
, and
to
; no semantic changes are intended.
Currently, feature test macro wording is not present. If [P2505R2] ends up adopting the macro changes, then I will provide an accompanied wording here.
5.1. 22.8.3 Unexpected objects [expected.unexpected]
5.1.1. 22.8.3.2 Class template unexpected
[expected.un.object]
5.1.1.1. 22.8.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 unex ; // exposition only }; template < class E > unexpected ( E ) -> unexpected < E > ; }
5.1.1.2. 22.8.3.2.2 Constructors [expected.un.ctor]
template < class Err = E > constexpr explicit unexpected ( Err && e );
Constraints:
-
isis_same_v < remove_cvref_t < Err > , unexpected > false
; and -
isis_same_v < remove_cvref_t < Err > , in_place_t > false
; and -
isis_constructible_v < E , Err > true
.
Effects: Direct-non-list-initializes
with
.
Throws: Any exception thrown by the initialization of
.
template < class ... Args > constexpr explicit unexpected ( in_place_t , Args && ... args );
Constraints:
is true
.
Effects: Direct-non-list-initializes
with
.
Throws: Any exception thrown by the initialization of
.
template < class U , class ... Args > constexpr explicit unexpected ( in_place_t , initializer_list < U > il , Args && ... args );
Constraints:
is true
.
Effects: Direct-non-list-initializes
with
.
Throws: Any exception thrown by the initialization of
.
5.1.1.3. 22.8.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.1.4. 22.8.3.2.4 Swap [expected.un.swap]
constexpr void swap ( unexpected & other ) noexcept ( is_nothrow_swappable_v < E > );
Mandates:
is true
.
Effects: Equivalent to:
5.1.1.5. 22.8.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. 22.8.4 Class template bad_expected_access
[expected.bad]
namespace std { template < class E > class bad_expected_access : public bad_expected_access < void > { public : explicit bad_expected_access ( E ); const char * what () const noexcept override ; E & error () & noexcept ; const E & error () const & noexcept ; E && error () && noexcept ; const E && error () const && noexcept ; private : E val unex ; // exposition only }; }
The class template
defines the type of objects thrown as exceptions to report the situation where an attempt
is made to access the value of an
object for which
is false
.
explicit bad_expected_access ( E e );
Effects: Initializes
with
.
const E & error () const & noexcept ; E & error () & noexcept ;
Returns:
.
E && error () && noexcept ; const E && error () const && noexcept ;
Returns:
.
5.3. 22.8.6 Class template expected
[expected.expected]
5.3.1. 22.8.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.3.2. 22.8.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.3.3. 22.8.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.4. 22.8.7 Partial specialization of expected
for void
types [expected.void]
5.4.1. 22.8.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.4.2. 22.8.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.4.3. 22.8.7.7 Equality operators [expected.void.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:
.