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

Created on 2022-11-30.00:00:00 last changed 3 weeks ago

Messages

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: 2022-11-30.00:00:00

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.ctor] 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-01-06 14:40:19adminsetmessages: + msg13171
2022-11-30 00:00:00admincreate