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

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

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.

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