Why does std::expected<T, E> require T to be copy-constructible if it is copy-assignable?

143 Views Asked by At

If we have a type T that is not copy-constructible but copy-assignable:

struct T {
    T() = default;
    ~T() = default;
    T(const T&) = delete;
    T(T&&) = default;
    T& operator=(const T&) = default;
    T& operator=(T&&) = default;
};

then std::is_copy_assignable_v<T> is obviously true, but std::is_copy_assignable_v<std::expected<T, int>> is false.

This behavior is described on cppreference: std::expected<T,E>::operator=

What is the rationale behind this? Why couldn't we allow std::expected<T, E> to be copy-assignable if T is copy-assignable, even if it is not copy-constructible? The same question also applies to move assignability.

2

There are 2 best solutions below

3
On BEST ANSWER

The result of the assignment depends on whether this->has_value() is true. We can assign an expected holding a T to an expected holding an E. In the case of holding an error, there is no "target T" to assign to, and we must copy construct to obtain a value.

Simple illustration:

std::expected<char, int> e1 = std::unexpected(2),
                         e2 = {'1'};

e1 = e2; // This copy initializes a char in e1 from the char in e2

Since this is all determined at runtime, both conditions must hold for the member to be well-defined on all code paths.

0
On

The std::expected<T, E> contains either a T object (expected value), or a E object (unexpected value).

In a program like this:

enum error_code { err1 = 1 };
std::expected<T, error_code> exp = std::unexpected(err1);
exp = T(1);
exp = T(2);

It would first create the exp object with only a error_code (unexpected) inside.

Then exp = T(1), it assigns it so that the exp now contains a T (expected) value instead. So it destructs the error_code object, and then copy-constructs the T object (from the one that is passed to it.) It needs to copy-construct it (instead of copy-assign) because the T object in exp has not been constructed at this point. (But the exp itself is).

In exp = T(2), the exp already has a T object, so it will instead copy-assign to it.

So because of the first case (when an existing std::expected<T, E> that has an unexpected value, it assigned an expected value), copy-assigning is only possible if the T object is copy-constructible.