Title
std::expected<bool, E1> conversion constructor expected(const expected<U, G>&) should take precedence over expected(U&&) with operator bool
Status
c++23
Section
[expected.object.cons]
Submitter
Hui Xie

Created on 2022-11-30.00:00:00 last changed 5 months ago

Messages

Date: 2023-02-13.11:31:32

Proposed resolution:

This wording is relative to N4928.

  1. Modify [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> is true,
    • (23.2) — is_same_v<remove_cvref_t<U>, in_place_t> is false, and
    • (23.3) — is_same_v<remove_cvref_t<U>, optional> is false, and
    • (23.4) — if T is cv bool, remove_cvref_t<U> is not a specialization of optional.

    -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 inside explicit 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&> is true, and
    • (28.1) — if T is not cv bool, converts-from-any-cvref<T, optional<U>> is false.

    -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> is true, and
    • (33.1) — if T is not cv bool, converts-from-any-cvref<T, optional<U>> is false.

    -34- Effects: If rhs contains a value, direct-non-list-initializes the contained value with std::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>

  2. Modify [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 be const U& for the first overload and U for the second overload.
    • (17.2) — GF be const G& for the first overload and G for the second overload.

    -18- Constraints:

    • (18.1) — is_constructible_v<T, UF> is true; and
    • (18.2) — is_constructible_v<E, GF> is true; and
    • (18.3) — if T is not cv bool, converts-from-any-cvref<T, expected<U, G>> is false; and
    • (18.4) — is_constructible_v<unexpected<E>, expected<U, G>&> is false; and
    • (18.5) — is_constructible_v<unexpected<E>, expected<U, G>> is false; and
    • (18.6) — is_constructible_v<unexpected<E>, const expected<U, G>&> is false; and
    • (18.7) — is_constructible_v<unexpected<E>, const expected<U, G>> is false.

    -19- Effects: If rhs.has_value(), direct-non-list-initializes val with std::forward>UF>(*rhs). Otherwise, direct-non-list-initializes unex with std::forward>GF>(rhs.error()).

    -20- Postconditions: rhs.has_value() is unchanged; rhs.has_value() == this->has_value() is true.

    -21- Throws: Any exception thrown by the initialization of val or unex.

    -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> is false; and
    • (23.2) — is_same_v<expected, remove_cvref_t<U>> is false; and
    • (23.3) — is_constructible_v<T, U> is true; and
    • (23.4) — remove_cvref_t<U> is not a specialization of unexpected; and
    • (23.5) — if T is cv bool, remove_cvref_t<U> is not a specialization of expected.

    -24- Effects: Direct-non-list-initializes val with std::forward>U>(v).

    -25- Postconditions: has_value() is true.

    -26- Throws: Any exception thrown by the initialization of val.

Date: 2023-02-13.00:00:00

[ 2023-02-13 Approved at February 2023 meeting in Issaquah. Status changed: Immediate → WP. ]

Date: 2023-02-10.01:18:02

[ Issaquah 2023-02-09; LWG ]

Move to Immediate for C++23

Date: 2023-02-09.02:09:16

[ Issaquah 2023-02-08; Jonathan provides wording ]

Date: 2023-01-15.00:00:00

[ 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.

Date: 2023-02-07.12:12:38

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.

However, if we change the type from 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.

This is surprising behaviour given how inconsistent between 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 ([expected.object.cons] p17):

  1. (17.3) — is_constructible_v<T, expected<U, G>&> is false; and

  2. (17.4) — is_constructible_v<T, expected<U, G>> is false; and

  3. (17.5) — is_constructible_v<T, const expected<U, G>&> is false; and

  4. (17.6) — is_constructible_v<T, const expected<U, G>> is false; and

  5. (17.7) — is_convertible_v<expected<U, G>&, T> is false; and

  6. (17.8) — is_convertible_v<expected<U, G>&&, T> is false; and

  7. (17.9) — is_convertible_v<const expected<U, G>&, T> is false; and

  8. (17.10) — is_convertible_v<const expected<U, G>&&, T> is false; 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.

I would suggest to special case 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.

History
Date User Action Args
2023-11-22 15:47:43adminsetstatus: wp -> c++23
2023-02-13 11:31:32adminsetmessages: + msg13391
2023-02-13 11:31:32adminsetstatus: immediate -> wp
2023-02-10 01:18:02adminsetmessages: + msg13326
2023-02-10 01:18:02adminsetstatus: new -> immediate
2023-02-09 02:09:16adminsetmessages: + msg13299
2023-02-09 02:09:16adminsetmessages: + msg13298
2023-01-06 14:40:19adminsetmessages: + msg13171
2022-11-30 00:00:00admincreate