Movable types

Crubit requires types to be “movable” to be passed by value: if a Rust type does not logically support a C++ move operation, then it can receive bindings, but it cannot be passed by value.

A Rust type can be made movable in C++ in one of three ways:

  1. Copyable: the Rust type implements Clone.
  2. Trivially move-constructible and destructible: the Rust type does not have a destructor. (It does not implement Drop, and nor do any of its fields.)
  3. Non-trivially move-constructible: the Rust type has a destructor, but implements Default.

The easiest way to ensure your type is useful to end users, even if it is changed in the future, is to implement Clone and Default. This makes the type default-constructible and copyable[^semiregular], as well as efficiently movable.

Copyable

If the Rust type implements Clone, then the C++ type will be copyable:

  • Copy construction has the same behavior as Clone::clone.
  • Copy assignment has the same behavior as Clone::clone_from.

Because the type is copyable, it is also movable, at worst by a copy operation.

Trivially move-constructible and destructible

If no logic occurs during destruction, because the type doesn't implement Drop, and none of its fields do, then the C++ type will be trivially-movable and trivially-destructible:

  • Move construction and assignment copy the bytes of the object, with the same behavior as a Rust move operation.

NOTE: All Copy types are guaranteed to be trivially move-constructible and destructible.

If the Rust type is Copy, then the moved-from object is guaranteed to hold its old value, and be valid for all operations.

Otherwise, the object is only valid for assignment and destruction, and the behavior of performing any other operation is undefined.

Non-trivially move-constructible

If the Rust type is not trivially movable and destructible, but implements Default, then the resulting C++ type will be (non-trivially) move constructible:

  • Move construction has the same behavior as std::mem::take(): it copies the bytes to the new object, as if by a Rust move, and replaces the moved-from object with Default::default().
  • Move assignment copies the bytes to the new object, as if by a Rust move, and replaces the moved-from object with an unspecified but valid object.

Why is this required?

In general, Crubit needs to be able to move objects as part of the implementation of pass-by-value, even in C++17, due to platform ABI restrictions. Even without this requirement, types are not very useful in C++ if they are not movable.

Unlike Rust, C++ has no “destructive move”. There is no way to change an object‘s location in memory, only to create a new object with the same value, and leave behind something in the old (still valid) object. Sometimes, what’s left behind is an identical copy of the object state: this is a copy operation, implemented by the C++ copy constructor or copy assignment operator. But sometimes, copying is expensive, and instead what we might leave behind is some kind of junk value. It still must be a valid object (at least so that its destructor and assignment operator can be invoked), but it might represent some invalid or moved-from state.

For example, to “move” a unique_ptr (the C++ equivalent of Box) from one variable to another, you copy the bytes, and then replace the old location with a special null value representing an unoccupied / moved-from unique_ptr. This is why unique_ptr must be nullable in the C++ type system: otherwise, it could not be moved!

[^semiregular]: The combination of default-constructible and copyable is so important for making types useful in C++ that it even has a name: “semiregular”