This page is a snapshot from the LWG issues list, see the Library Active Issues List for more information and the meaning of New status.

4222. expected constructor from a single value missing a constraint

Section: 22.8.6.2 [expected.object.cons] Status: New Submitter: Bronek Kozicki Opened: 2025-03-12 Last modified: 2025-03-15

Priority: Not Prioritized

View all other issues in [expected.object.cons].

View all issues with New status.

Discussion:

When an expected object is initialized with a constructor taking first parameter of type unexpect_t , the expectation is that the object will be always initialized in disengaged state (i.e. the user expected postcondition is that has_value() will be false), as in the example:

struct T { explicit T(auto) {} };
struct E { E() {} };

int main() {
  expected<T, E> a(unexpect);
  assert(!a.has_value());
}

This does not hold when both value type T and error type E have certain properties. Observe:

struct T { explicit T(auto) {} };
struct E { E(int) {} }; // Only this line changed from the above example

int main() {
  expected<T, E> a(unexpect);
  assert(!a.has_value()); // This assert will now fail
}

In the example above the overload resolution of a finds the universal single parameter constructor for initializing expected in engaged state (22.8.6.2 [expected.object.cons] p23):

template<class U = remove_cv_t<T>>
  constexpr explicit(!is_convertible_v<U, T>) expected(U&& v);

This constructor has a list of constraints which does not mention unexpect_t (but it mentions e.g. unexpected and in_place_t). Email exchange with the author of expected confirmed that it is an omission.

The proposed resolution is to add the following additional constraint to this constructor:

is_same_v<remove_cvref_t<U>, unexpect_t> is false

This will result in the above, most likely buggy, program to become ill-formed. If the user intent was for the object to be constructed in an engaged state, passing unexpect_t to the T constructor, they can fix the compilation error like so:

expected<T, E> a(in_place, unexpect);

Proposed resolution:

This wording is relative to N5001.

  1. Modify 22.8.6.2 [expected.object.cons] as indicated:

    template<class U = remove_cv_t<T>>
    constexpr explicit(!is_convertible_v<U, T>) expected(U&& v);
    

    -23- Constraints:

    1. (23.1) — is_same_v<remove_cvref_t<U>, in_place_t> is false; and

    2. (23.2) — is_same_v<expected, remove_cvref_t<U>> is false; and

    3. (23.?) — is_same_v<remove_cvref_t<U>, unexpect_t> is false; and

    4. (23.3) — remove_cvref_t<U> is not a specialization of unexpected; and

    5. (23.4) — is_constructible_v<T, U> is true; and

    6. (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.