Note: None of this has been implemented.
Values are usually copied as they flow from variable-to-variable or function-to-function. Changes made to a copy won'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 that we can't or shouldn't copy. Uncopyable values can be useful:
- When the value depends on some other stateful resource(s) such that creating a copy 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 graphs) from one thread to another, without requiring the use of performance-degrading locks or atomics.
This page explains what makes a value copyable or not, and then describes how move semantics allow an uncopyable value to move from one part of a program to another.
Move vs. Copy Types
Whether a value is copyable or not is largely determined by the value's type. The compiler uses heuristics to infer whether or not a type's values are copyable, based on examining the fields and methods it implements:
- If the clone method is implemented, the type's values may be copied. Since the compiler automatically uses the clone 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 may not be copied. 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 cannot be copied, the type's values may not be copied. This makes sense: if part of a value cannot be copied, the whole value certainly cannot be either.
- If any of the type's fields are references to other data structures, the type's values may not be copied. This is because it is not immediately obvious how to automatically and safely create copies of this type's values.
- If none of the above apply, the type's values may be copied.
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. This capability is called move semantics, which differs from the copy semantics we have worked with so far. Non-copyable values use move semantics. Copyable values use copy semantics.
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.
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 = 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 = 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
Deactivation of the source happens even for conditional moves, where one branch of conditional move 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.
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.)
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 = 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 and Swaps
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:
- A non-copyable value held by a named field in a struct may be moved out. However, doing so invalidates any future access to the variable holding that struct, including any values held by its other fields.
- It is prohibited to move out a non-copyable value held by some indexed element in an array.
These are significant limitations on the use of non-copyable values in structs and arrays. However, these limitations can be ameliorated by using the swap operator:
struct Connection: s Socket b i32 mut conn = Connection[Socket(), 10] mut x = 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.
Assignment is an expression whose value (for copy semantics) is a copy of the value of the right-hand expression. With a move-only value, however, the value of the assignment expression is the value previously stored in the left-hand (lval) memory location.
mut conn = Connection[Socket(), 10] mut x = conn.s = Socket() // conn now contains the new socket and x contains the old one