The class template std::optional manages an optional contained value, i.e. a value that may or may not be present[1]. It is a great alternative for std::unique_ptr, or raw pointer, when used solely to express that a value is indeed optional. The type of the variable explicitly states that a contained variable is optional and it is stored by value.

The following is an example of a function that returns a value, if it exists, or null optional:

1
2
3
4
5
6
std::optional<Object> get();

auto object = get();
if (object) {
    // Use object
}

Since C++ 11 allows the “movement” of variables, the below demonstrates what happens when std::optional is moved:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
template <typename T>
void print(const T &o, const std::string &name) {
    std::cout << name << ".has_value(): "
              << std::boolalpha
              << o.has_value()
              << std::noboolalpha << std::endl;
    if (o.has_value()) {
        std::cout << name
              << ".value(): '" << o.value() << "'"
              << std::endl
              << std::endl;
    }
}

int main(int argc, char *argv[]) {
    auto i1 = std::make_optional<int>(42);
    auto i2 = std::move(i1);
    print(i1, "i1");
    print(i2, "i2");
    auto s1 = std::make_optional<std::string>("hello");
    auto s2 = std::move(s1);
    print(s1, "s1");
    print(s2, "s2");
    return 0;
}

Output:

i1.has_value(): true
i1.value(): '42'

i2.has_value(): true
i2.value(): '42'

s1.has_value(): true
s1.value(): ''

s2.has_value(): true
s2.value(): 'hello'

This example portrays how std::move does not change the state of optional if the contained value is primitive, as move for primitives just copies the values. For objects like std::string, it actually moves the value (and empties the string contained in s1) but s1 still has_value.
The following visuals illustrate the various states of std::optional<std::string> discussed thus far.

std::optional layout

cppreference.com provides a clear explanation of the behavior:

Move constructor: If other contains a value, initializes the contained value as if direct-initializing (but not direct-list-initializing) an object of type T with the expression std::move(*other) and does not make other empty: a moved-from optional still contains a value, but the value itself is moved from. If other does not contain a value, constructs an object that does not contain a value. This constructor does not participate in overload resolution unless std::is_move_constructible_v is true. It is a trivial constructor if std::is_trivially_move_constructible_v is true.

The behavior is perfectly legal in terms of the standard, but can seem illogical from the user’s perspective, who would naturally expect the optional to have no value if it has been moved. In order to eliminate all doubt, I would suggest to std::optional::reset after moving an optional object.
The reset would call the destructor of the contained object (if the destructor exists) and set the has_value flag to false.

1
2
3
4
5
6
    ...
    i1.reset();
    print(i1, "i1");
    s1.reset();
    print(s1, "s1");
}

Output:

...
i1.has_value(): false
s1.has_value(): false

Special thanks to Petar Ivanov for the idea and Rina Volovich for editing.