A moved from optional

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?

  1. Because for optional move semantics can be implemented without it.
  2. 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.

This entry was posted in programming and tagged , , , . Bookmark the permalink.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.