std::expected<bool, E1>
conversion constructor expected(const expected<U, G>&)
should take precedence over expected(U&&)
with operator bool
Section: 22.8.6.2 [expected.object.cons] Status: C++23 Submitter: Hui Xie Opened: 2022-11-30 Last modified: 2023-11-22
Priority: 1
View all issues with C++23 status.
Discussion:
The issue came up when implementing std::expected
in libc++. Given the following example:
struct BaseError{};
struct DerivedError : BaseError{};
std::expected<int, DerivedError> e1(5);
std::expected<int, BaseError> e2(e1); // e2 holds 5
In the above example, e2
is constructed with the conversion constructor
expected::expected(const expected<U, G>&)
and the value 5
is correctly copied into e2
as expected.
int
to bool
, the behaviour is very surprising.
std::expected<bool, DerivedError> e1(false);
std::expected<bool, BaseError> e2(e1); // e2 holds true
In this example e2
is constructed with
expected::expected(U&&)
together with
expected::operator bool() const
Instead of copying e1
's "false
" into e2
, it uses operator bool
, which returns
true
in this case and e2
would hold "true
" instead.
int
and bool
.
The reason why the second example uses a different overload is that the constructor
expected(const expected<U, G>& rhs);
has the following constraint
(22.8.6.2 [expected.object.cons] p17):
(17.3) —
is_constructible_v<T, expected<U, G>&>
isfalse
; and(17.4) —
is_constructible_v<T, expected<U, G>>
isfalse
; and(17.5) —
is_constructible_v<T, const expected<U, G>&>
isfalse
; and(17.6) —
is_constructible_v<T, const expected<U, G>>
isfalse
; and(17.7) —
is_convertible_v<expected<U, G>&, T>
isfalse
; and(17.8) —
is_convertible_v<expected<U, G>&&, T>
isfalse
; and(17.9) —
is_convertible_v<const expected<U, G>&, T>
isfalse
; and(17.10) —
is_convertible_v<const expected<U, G>&&, T>
isfalse
; and
Since T
is bool
in the second example, and bool
can be constructed from
std::expected
, this overload will be removed. and the overload that takes U&&
will be selected.
bool
, i.e.
(The above 8 constraints); or
is_same_v<remove_cv_t<T>, bool>
is true
And we need to make sure this overload and the overload that takes expected(U&&)
be mutually exclusive.
[2023-01-06; Reflector poll]
Set priority to 1 after reflector poll.
There was a mix of votes for P1 and P2 but also one for NAD
("The design of forward/repack construction for expected matches optional,
when if the stored value can be directly constructed, we use that.").
std::optional<bool>
is similarly affected.
Any change should consider the effects on
expected<expected<>>
use cases.
[Issaquah 2023-02-08; Jonathan provides wording]
[Issaquah 2023-02-09; LWG]
Move to Immediate for C++23
[2023-02-13 Approved at February 2023 meeting in Issaquah. Status changed: Immediate → WP.]
Proposed resolution:
This wording is relative to N4928.
Modify 22.5.3.2 [optional.ctor] as indicated:
template<class U = T> constexpr explicit(see below) optional(U&& v);
-23- Constraints:
[Drafting note: Change this paragraph to a bulleted list.]
- (23.1) —
is_constructible_v<T, U>
istrue
,- (23.2) —
is_same_v<remove_cvref_t<U>, in_place_t>
isfalse
,and- (23.3) —
is_same_v<remove_cvref_t<U>, optional>
isfalse
, and- (23.4) — if
T
is cvbool
,remove_cvref_t<U>
is not a specialization ofoptional
.-24- Effects: Direct-non-list-initializes the contained value with
std::forward>U>(v)
.-25- Postconditions:
*this
has a value.-26- Throws: Any exception thrown by the selection constructor of
T
.-27- Remarks: If
T
's selected constructor is a constexpr constructor, this constructor is a constexpr constructor. The expression insideexplicit
is equivalent to:
!is_convertible_v<U, T>template<class U> constexpr explicit(see below) optional(const optional<U>& rhs);
-28- Constraints:
- (28.1) —
is_constructible_v<T, const U&>
istrue
, and- (28.1) — if
T
is not cvbool
,converts-from-any-cvref<T, optional<U>>
isfalse
.-29- Effects: If
rhs
contains a value, direct-non-list-initializes the contained value with*rhs
.-30- Postconditions:
rhs.has_value() == this->has_value()
.-31- Throws: Any exception thrown by the selection constructor of
T
.-32- Remarks: The expression inside
explicit
is equivalent to:
!is_convertible_v<const U&, T>template<class U> constexpr explicit(see below) optional(optional<U>&& rhs);
-33- Constraints:
- (33.1) —
is_constructible_v<T, U>
istrue
, and- (33.1) — if
T
is not cvbool
,converts-from-any-cvref<T, optional<U>>
isfalse
.-34- Effects: If
rhs
contains a value, direct-non-list-initializes the contained value withstd::move(*rhs)
.rhs.has_value()
is unchanged.-35- Postconditions:
rhs.has_value() == this->has_value()
.-36- Throws: Any exception thrown by the selection constructor of
T
.-37- Remarks: The expression inside
explicit
is equivalent to:
!is_convertible_v<U, T>
Modify 22.8.6.2 [expected.object.cons] as indicated:
template<class U, class G> constexpr explicit(see below) expected(const expected<U, G>& rhs); template<class U, class G> constexpr explicit(see below) expected(expected<U, G>&& rhs);
-17- Let:
- (17.1) —
UF
beconst U&
for the first overload andU
for the second overload.- (17.2) —
GF
beconst G&
for the first overload andG
for the second overload.-18- Constraints:
- (18.1) —
is_constructible_v<T, UF>
istrue
; and- (18.2) —
is_constructible_v<E, GF>
istrue
; and- (18.3) — if
T
is not cvbool
,converts-from-any-cvref<T, expected<U, G>>
isfalse
; and- (18.4) —
is_constructible_v<unexpected<E>, expected<U, G>&>
isfalse
; and- (18.5) —
is_constructible_v<unexpected<E>, expected<U, G>>
isfalse
; and- (18.6) —
is_constructible_v<unexpected<E>, const expected<U, G>&>
isfalse
; and- (18.7) —
is_constructible_v<unexpected<E>, const expected<U, G>>
isfalse
.-19- Effects: If
rhs.has_value()
, direct-non-list-initializesval
withstd::forward>UF>(*rhs)
. Otherwise, direct-non-list-initializesunex
withstd::forward>GF>(rhs.error())
.-20- Postconditions:
rhs.has_value()
is unchanged;rhs.has_value() == this->has_value()
istrue
.-21- Throws: Any exception thrown by the initialization of
val
orunex
.-22- Remarks: The expression inside
explicit
is equivalent to!is_convertible_v<UF, T> || !is_convertible_v<GF, E>
.template<class U = T> constexpr explicit(!is_convertible_v<U, T>) expected(U&& v);
-23- Constraints:
- (23.1) —
is_same_v<remove_cvref_t<U>, in_place_t>
isfalse
; and- (23.2) —
is_same_v<expected, remove_cvref_t<U>>
isfalse
; and- (23.3) —
is_constructible_v<T, U>
istrue
; and- (23.4) —
remove_cvref_t<U>
is not a specialization ofunexpected
; and- (23.5) — if
T
is cvbool
,remove_cvref_t<U>
is not a specialization ofexpected
.-24- Effects: Direct-non-list-initializes
val
withstd::forward>U>(v)
.-25- Postconditions:
has_value()
istrue
.-26- Throws: Any exception thrown by the initialization of
val
.