Move Types and Semantics
On this page
Note: Assigning a value to the anonymous variable _ to destroy it early is not implemented: the name is not recognized. A clone method takes no part in copying: a type may declare one and call it by name, but no copy calls it, so it does not make a type copyable, and a type with a finalizer is still a move type. A clone method written with the new permission is refused. There is no compile-time if. The rule that a type holding a reference to other data moves is not yet enforced as written: a struct whose fields are borrowed references (&, &mut, &imm) or shared owning references (Rc[mut, T], Rc[imm, T]) is copied, and only an owning So or a uni field makes it move. The rule that a non-copyable value may not be moved out of an indexed array element is not yet enforced: such a move is accepted and deactivates the whole array, and the elements left in it are never finalized.
Values are copied as they flow from variable-to-variable or function-to-function. Changes made to a copy don't affect the original value.
imm a = 3; mut b = a; // b holds a copy of a's value b += 1; // b's value is changed to 4 a; // is still 3
Sometimes, however, we have values for which we don't want multiple copies to exist at the same time. Such "non-copyable" values can be useful:
- When the value depends on a stateful resource(s), where the existence of a copy of this value would confuse how we interact with the resource(s). For example, imagine the value is a handle to a just-opened file and we make a copy of that handle as another value. If we close the file using one handle, it might then be unsafe to use the other handle.
- When it is not clear how to create a faithful copy of a complex value, particularly when insufficient metadata exists or when doing so involves memory or resource allocation or establishing appropriate external dependencies.
- When we want to take advantage of the single-owner memory management strategy. If an allocated value never needs to be copied, single-owner offers performance and determinacy advantages over reference-counting and tracing GC.
- When we want to transfer mutable data (including complex data graphs) from one thread to another, without requiring the use of performance-degrading locks or atomics.
To provide safe support for such "non-copyable" values, Cone supports move semantics. Move semantics enable non-copyable values to be moved from one place to another in a code's logic. However, once the value moves, it may no longer be accessed from its old location. This restriction ensures there is only ever one copy of the value in existence.
Move vs. Copy Types#
Whether a value is handled by move vs. copy semantics is determined by its type. Every type is exactly one of two things, named by two built-in marker traits: Move or Copy. In general, Cone treats most types as having copy semantics, unless it detects some reason why move semantics restrictions are needed; the compiler grants the type the trait that follows from these rules:
- If the type declares that it uses move semantics, then it does. This is as simple as
naming the built-in trait Move in the type's
is list:
struct Handle is Move { pub id i32; }So does a type declaring AtomicValue, the marker of an atomic value: a copy of one would be a second value that no atomic operation on the first could reach. A generic type may declare it for only some of its instances (Move for some instances). - If the type implements an initializer method that essentially enables the creation of a copy, the type's values have copy semantics. Since the compiler automatically uses this cloning method (when implemented) to make all copies of values of this type, these values are clearly copyable.
- If the final method is implemented, the type's values have move semantics. If the type requires a finalizer to clean up dependencies, chances are excellent that creating copies will open the door to safety issues.
- If any of the type's fields have move semantics, the type's values have move semantics. This is because if part of a value cannot be copied, the whole value certainly cannot be either.
- If the type is an enum and any of its variants has move semantics, by any of these rules, the enum's values have move semantics, since a value of the enum may be that variant.
- If any of the type's fields are references to other data structures, the type's values have move semantics. This is because it is not immediately obvious how to automatically and safely create copies of this type's values.
- If the type is marked as having move semantics, that's what it gets.
- If none of the above apply, the type's values have copy semantics.
A type may also declare is Copy. That is an assertion, not an instruction: it changes nothing about a type that copies anyway, and the compiler refuses it on a type the rules above make a move type — one that also declares Move, or has a finalizer, a field that moves, a base it enriches that moves, or (for an enum) a variant that moves.
struct Point is Copy { // accepted: nothing here moves
pub x i32;
pub y i32;
}
struct Log is Copy { // **ERROR**: its finalizer makes it move
fn final(self &uni) { }
}
Move-ness is a trait rather than an attribute because it is a fact other code may ask about, which is what a trait is for; an attribute is an instruction to the compiler about representation or linkage that no code asks about. A generic asks by requiring where T is Copy or where T is Move (Generic constraints). There is no compile-time if to ask it inside a body: v is Move in an expression is the variant test, so it is refused on a value that has no variants.
An owning reference is a move type when its permission forbids aliasing, or when its region ref declares Move, as So does (Regions).
Move for some instances#
A generic type that holds a value of its type parameter already moves exactly when that parameter does, by rule 4: a Box[i32] holding an i32 copies, and a Box[Handle] holding a Handle moves. A generic type that holds no value of its parameter, but stands for one, has no such field for rule 4 to find. A future is the usual case: it holds a shared reference to a cell where the result will be put, and a shared reference copies whatever it points at. Such a type says when it moves with a condition on the Move entry of its is list, written after the word if:
struct Handle is Move {
pub id i32;
}
struct Cell[T] {
pub v T;
}
struct Future[T] is Move if T is Move {
pub c Rc[imm, Cell[T]];
}
imm f = new Future[i32](new Rc[imm, Cell[i32]](new Cell[i32](5)));
imm f2 = f; // a copy: i32 does not move, so neither does Future[i32]
imm f3 = f; // another copy; f is still there
imm g = new Future[Handle](new Rc[imm, Cell[Handle]](new Cell[Handle](new Handle(7))));
imm g2 = g; // moves g: Handle moves, so Future[Handle] does
imm g3 = g; // **ERROR** g was moved out
In plain words: a Future of something that moves moves too, so the result inside it can be taken only once, and a Future of something that copies can be copied freely. The actors package's Future[T], a behaviour's value kept to be awaited later, is declared so (Storing a future).
Each instance decides the condition with its own type arguments. Where the condition holds, the instance is Move. Where it does not, the instance lacks the entry, so it copies, unless one of the other rules above makes it move: a condition can add Move, never take it away. A Future holding a Handle field of its own would move whatever its condition said.
The condition is written the way a where clause's is (Where clause): clauses T is Name, joined with and and or and grouped by parentheses, each about one of the type's own type parameters. A condition may ask about one parameter of several: struct Pair[A, B] is Move if B is Move moves only when its second argument does.
Copy takes no condition. A type copies wherever nothing makes it move, so the condition is always written on Move, and an instance that does not meet it copies. A type with no type parameters takes no condition either: every use of it is the same type, so there is nothing for a condition to vary.
The condition belongs to the one entry it follows. A where clause after the is list says something different: it is a requirement on the whole type. struct Future[T] is Move where T is Move makes every instance move, and refuses Future[i32] outright, since i32 does not meet the requirement.
Move Capabilities and Restrictions#
Even when we want to prohibit the copying of these values, we still want to be able to move such values around from variable-to-variable or function-to-function.
Moving values around looks just like copying, but adds restrictions to ensure multiple copies of the value are never made. These restrictions, if not well understood, can make it frustrating to write programs that do what is desired in a way that the compiler deems acceptable.
Source Deactivation#
It would be misleading to think that moving a value does not copy it. In fact, a "shallow" copy of the value might well be made on every move. What the move restrictions guarantee is that only one version of the value ever exists at a time.
This guarantee is implemented by deactivating the value's source as part of the move. When the source is a variable, this prohibits post-move access to that variable's value:
imm socket = new Socket(); // Open a new network socket (move semantics) imm sock2 = socket; // Move the value to sock2 socket.poll(); // **ERROR** socket no longer has an accessible value
By de-activating use of the variable socket after the move, we effectively have only one "copy" of its value, which is now in sock2. If we had not deactivated socket, we would then have two copies of a non-copyable value.
The same deactivation mechanism applies when passing a non-copyable value to another function.
imm socket = new Socket(); // Open a new network socket (move semantics) afunction(socket); // the socket is moved to the called function anotherfunc(socket); // **ERROR**: socket is deactivated and unusable
A value handed on by a block, an if or a loop is moved just the same, and deactivates its source:
imm socket = new Socket();
imm moved = { socket; }; // the block hands socket on: it moves
afunction(socket); // **ERROR**: socket is deactivated and unusable
A non-copyable value may be returned by a called function. There is no need to deactivate the value's source, since it is already gone.
(Note: A deactivated source variable may be re-activated at any point by giving it a new value.)
Conditional moves and drop flags#
Deactivation of the source happens even for conditional moves, where one branch moves the value but another does not. When the branches join together again, the compiler can't know whether the value is there or not, so conservatively, it assumes the value is gone: the variable may not be used again until it is given a new value. A deactivated source may be neither read nor borrowed.
imm socket = new Socket();
if done {
afunction(socket); // moved on this path only
}
socket.poll(); // **ERROR**: socket may have been moved
peek(&socket); // **ERROR**: nor may it be borrowed
The same holds for a move in a loop: a value moved in one pass is not there in the next, so moving or using a variable declared outside the loop after it may have moved is an error.
imm socket = new Socket();
mut n = 0;
while n < 3 {
afunction(socket); // **ERROR**: socket may have been moved by an earlier pass
n = n + 1;
}
The value itself is still destroyed exactly once. On the path that moved it, its new holder destroys it; on the path that did not, it is destroyed at the end of its scope, as any value is. The compiler keeps a hidden drop flag beside such a variable, set where its value arrives and cleared where the value moves out, and the end of the scope tests it: the value is finalized there only if the flag says it is still there. A variable moved on every path, or on none, needs no flag and costs nothing.
struct Socket is Move {
pub id i32;
fn final(self &uni) { print <- "close "; print <- id; print <- "\n"; }
}
fn consume(s Socket) { print <- "consume "; print <- s.id; print <- "\n"; }
fn run(done bool, id i32) {
imm socket = new Socket(id);
if done {
consume(socket); // moved on this path only
}
print <- "end of run\n";
} // socket is closed here only if done was false
run(true, 1) prints consume 1, close 1 (consume's parameter dies at its end) and end of run. run(false, 2) prints end of run and then close 2. Neither closes a socket twice.
The same flag serves a value that is given a new value on only some paths, or assigned over (finalizers).
Scope-surfing and Destruction#
This movement of a non-copyable value can be understood as shortening or lengthening the value's lifetime as it surfs from one scope to another. When it fails to surf out of its last scope, the value will be destroyed at the end of that scope, as it is no longer useful.
To destroy a move value more quickly, just assign it to the anonymous variable "_":
imm socket = new Socket(); // Open a new network socket (move semantics) _ = socket; callfn(socket); // **ERROR**. socket is deactivated
Doing this on a copyable value has no effect.
Field/Element Moves#
If we want to move a non-copyable value out of a struct or array, things get more complicated, as other values are now potentially affected by such a move:
- It is prohibited to move out a non-copyable value held by a named field in a struct,
or by an element of a tuple.
The struct would be left with a hole in it: it could no longer be used as a whole,
and there would be no sound answer to what destroying it should finalize.
This holds wherever the struct is (a variable, a parameter, a by-value
self, a temporary, or reached through a reference) and wherever the value would go (into a variable, into a call, into a literal, or back to a caller, directly or as the value of a block, an if or a loop). It also holds for a value reached through a field, such as the value that an owning reference held in a field points at, or an element of an array held in a field. Moving the whole struct is always allowed, and takes every field with it. One case is a move of the whole struct in disguise, and is allowed: a match that takes what a variant holds out through its binding (all of a variant declared as a type, such as Some, and the one field of a struct variant), where the matched value is held only by the match (the answer of a call, such as names.pop(), or a variable the match consumes), and the variant holds nothing else and has no finalizer of its own. The move counts as moving the matched value, so nothing is left with a hole in it and nothing is finalized twice: match names.pop() { case mut s Some { imm last = s; ... } case is None { } }. A variant with two fields, or with its own finalizer, is still refused, and so is taking the contents twice. This is what lets each take an item that moves out of a cursor's Option (Each and Iteration). - It is prohibited to move out a non-copyable value held by some indexed element in an array.
- It is prohibited to move a non-copyable value out through a borrowed reference,
whether by dereferencing it or by reaching a field or element through it,
and whatever the reference's permission.
The borrowed reference does not own the value: the place it borrows from does,
and that place still holds the value after the borrow expires.
This applies wherever a value can go: into a variable, into a call
(including a method whose
selfis taken by value), into a literal, or back to a caller. - For the same reason, it is prohibited to move a non-copyable value out through a shared owning reference: one whose permission allows it to be aliased, such as Rc[mut, T], Rc[imm, T] or Rc[ro, T]. Such a reference is one of possibly many holders of the value, and the others would still point at it after the move. So is one into a region ref that declares neither aliasRef nor Move, whose references are shared freely. A sole owner may give its value up: a So reference, or a Rc[uni, T] one (which is what Rc means), since a unique reference is the only holder there is. Moving the value out deactivates the reference, as moving out of a variable does.
struct Connection {
pub s Socket;
pub b i32;
}
imm conn = new Connection(new Socket(), 10);
imm s = conn.s; // **ERROR**: may not move a field out of its struct
imm conn2 = conn; // the whole struct moves, its socket with it
These are significant limitations on the use of non-copyable values in structs and arrays. However, these limitations can be ameliorated by using either the swap or left-assignment operator:
Swap Operator#
The swap operator may be used on copy or move values:
struct Connection {
pub s Socket;
pub b i32;
}
mut conn = new Connection(new Socket(), 10);
mut x = new Socket(); // Open another socket
conn.s <=> x;
// conn now contains the new socket and x contains the old one
Swapping offers a helpful way to get around restrictive moves. One can temporarily extract a 'move' value out of a field or array element by swapping it with some arbitrary type-compatible value. After making use of the extracted value, simply swap it back in.
The reason why this works is that we end up with no copies of either value and the source still has a valid value (even if it is only a dummy value). That ensures that the original source stays safe to use.
Left-Assignment#
Assignment is an expression whose value (for copy semantics) is a copy of the value of the right-hand expression. With move-only semantics, it can be very helpful to have the value of the assignment expression be the value previously stored in the left-hand (lval) memory location. To obtain this result, use the left-assignment operator (:=).
mut conn = new Connection(new Socket(), 10); mut x = conn.s := new Socket(); // conn now contains the new socket and x contains the old one