blob: 30c3e1ab24844aa988ed36c8d269e83d4f20e3e7 [file] [log] [blame] [view]
# 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**](#copyable): the Rust type implements `Clone`.
2. [**Trivially move-constructible and destructible**](#trivial): the Rust type
does not have a
[destructor](https://doc.rust-lang.org/reference/destructors.html). (It does
not implement `Drop`, and nor do any of its fields.)
3. [**Non-trivially move-constructible**](#nontrivial): 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 {#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 {#trivial}
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 {#nontrivial}
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()`](https://doc.rust-lang.org/std/mem/fn.take.html): 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? {#why}
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"](https://en.cppreference.com/w/cpp/concepts/semiregular)