This post is in response to claims, that I have heard number of times, that the semantics of optional
’s move operations are wrong:
template <typename T> void test(T v) { optional<T> o = v; assert (o); // o contains a value optional<T> p = std::move(o); assert (o); // o still contains a value! }
Some people are surprised that the second assert holds: the unfulfilled expectation is that moving from an optional
should “eject” the value and leave the object in the state of not containing any value. While such semantics could be made to work, there are good reasons for preferring the present behavior.
What we discuss in this post applies to std::optional
and boost::optional
alike.
Actually, the expectation that a moved-from object should be left in a specific state is misguided. Move semantics were introduced for the purpose of:
- avoiding unnecessary resource allocations,
- providing the unique resource ownership model.
but neither of these requires the moved-from object to be in a special “null” or “empty” state. Even std::unique_ptr
doesn’t always become a null pointer after being moved from! Consider:
std::unique_ptr<int> p = std::make_unique<int>(1); p = std::move(p); // move-assign to self assert (p); // not null
This may be surprising if you expect that being null is a postcondition of the move assignment, but it is not. The guarantee of the move assignment is that:
- invariants of
unique_ptr
hold, - only one
unique_ptr
owns a resource, - no resource is leaked.
To be precise, unique_ptr
does guarantee that the moved-from object is a null pointer for any case other than the self-assignment, but this is the least interesting guarantee here. This is only the side effect of providing the guarantees expressed above. For other types, we have more options to implement this guarantee. For instance, this is how optional
satisfies the properties of move semantics:
int * p = new int(1); optional<unique_ptr<int>> op = unique_ptr<int>(p); assert (op); assert (op->get() == p); optional<unique_ptr<int>> o2 = std::move(op); assert (op); // still contains a value assert (op->get() == nullptr); // the value is moved from assert (o2); assert (o2->get() == p);
Even though after the move we are storing two unique_ptr
’s, only one of them owns the resource. But why not set op
to no-value state anyway?
- Because for
optional
move semantics can be implemented without it. - Because it would be unnecessarily slow.
Why are the present semantics of optional
faster? Because they allow the implementations to provide a trivially-copyable specialization of optional<T>
for trivial types T
. The implementer of the Standard Library, or of Boost.Optional, can provide the following specialization:
template <typename Trivial> class optional<Trivial> { bool _is_initialized; Trivial _value; optional() : _is_initialized(false) {} // use defaulted copy and move };
Now, with the defaulted move constructor or move assignment, compiler can implement a series of optimizations when these objects are copied or moved, one such provision that he compiler obtains is that it can just copy bits with memcpy()
. Further, type array<optional<int>, N>
is now also trivially-copyable, and sunbject for compiler optimizations, in particular, it can also be moved using memcpy()
. In contrast, if we were to set member _is_initialized
to false
for all N array elements, it would be an unnecessary overhead.
For the case where the optional must be left in an empty state, it is possible to do it with `T val = std::exchange(optional, std::nullopt);` which expresses the intent explicitly.