Edit: Rev. 2
Edit: Rev. 3
...
Edit: Rev. 6
Thank you all.
Original proposal
- Feature Name: Move references
- Start Date: 2021-04-17
- RFC PR: rust-lang/rfcs#0000
- Rust Issue: rust-lang/rust#0000
Summary
Move reference is a new kind of references that are intended to allow moving the value, but not the memory it's stored in and ease the story of initialization, essentially allowing more cases of its deferred and partial variants.
Motivation
- Make
Box
less special by creating mechanism (DerefMove) of moving out of a reference. - Create a sound mechanism for partial initialization and deinitialization of a bindings.
- Enable placement new to be just a language construct.
Guide-level explanation
&move
references provide a way to borrow the memory of a binding while preserving the logic of moving its value.
The type &move T
is, in fact, a reference, but unlike other references it allows moving out initialized value of referenced binding.
There are a few types of move references: plain and annotated with !
or *
.
About the functionality:
&move T |
&move T* |
&move T! |
---|---|---|
Allows to move out | Allows to move in | Allows both |
Allowing to move out a value implies that it is initialized. So referencing an uninitialized binding by &move T
or &move T!
is prohibited.
Moving a value into a binding, thus making, or keeping it initialized, is a part of &move T*
and &move T!
contracts. This is assumed to be true by the compiler, and used in corresponding checks.
Partial initialization of a binding of a known type C
is described via following syntax: &move C(a!,b*,c,...)
.
An example:
struct C {
a: String,
b: String,
c: String,
d: u32,
}
/// ...Promises to init `b`, keep `a` and uninit `c`, doesn't touch `d` at all.
fn work(arg: &move C(a!,b*,c,.d)) { //dot prefixed `d` may have been omitted.
let mut tmp = arg.a; //we moved the String to `tmp`
tmp.append(&arg.c) //we may not move the 'arg.c', but we haven't gave a promise to initialize it back.
arg.a = tmp; //we initialized `arg.a` back; removing this line is hard error.
arg.b = "init from another function!".into();
//println!("{:?}",arg.d ); //error: use of possibly uninitialized value.
}
fn main() {
let trg: C;
trg.a = "Hello ".into();
trg.c = " Hola".into();
work(&move trg);
println!(&trg.b); //legal, as work gave a promise to initialize
println!(&trg.a); //legal
//println!(&trg.c); //error: use of definitely uninitialized value.
}
The use case &move T*
reference family is initialization: these references may be used to describe placement features.
&move T!
is a move reference to initialized binding with ability to move from it. In fact it can be viewed as mutable reference. The reasons of creating it are simple:
- It doesn't change existing behavior of
&mut T
. - It is easily implied as a logical continuation of all the syntax of the feature.
Fields of a known type that are unaffected by an operation are simply not mentioned in the header of a move reference type.
Syntax of &move
references to a tuples is following:
Given a tuple (u32,i64,String,&str)
the move reference syntax is like: &move (.u32,i64,String!,&str*)
- note the dot prefixed u32
- it will not be touched by a consumer of a reference, but is here to distinguish different tuple types from one another (in cases of named structures untouched fields are simply not mentioned).
Reference-level explanation
Creation
It may be obvious, but creating a &move ..
reference is only possible for local bindings and bindings referenced via another &move ..
.
Interaction with patterns:
We introduce a new pattern kind ref move NAME
: this produces NAME
of type &move T!
. The reason of the !
obligation is that we may not want to left a binding (partially) deinitialized after execution of a pattern-matching construct.
Subtyping:
move
references have the following subtyping rules:
-
&move T!
is a subtype of&move T*
for allT
. -
&move T(*list of covered fields*)
is a subtype of&move T(*another list*)
for allT
if and only if first list mentions exactly the same fields as does second, and every mentioned field of the first guarantees the same as corresponding field of the second.
DerefMove
trait
I also propose design of DerefMove
:
trait DerefMove {
type Output;
fn deref(&move self!) -> &move Self::Output!;
}
The reason of such design is that we may not want allow pattern matching to partially deinitialize a binding, as it will require way more complex analysis as well as will give pattern matching powers that do not align well with its original purpose (expressing "high level" logic).
Aliasing:
Given that all move references are intended to modify referenced binding they all must be unique as &mut T
is.
Interaction with panics:
The proposal is said to rely on current move behavior: byte-level copy. However these may be deleted during optimizations. This leads to concerns like "what if placement function or function with temporary move panics?":
It's matter of observability: if no one will ever see corrupted data, why to avoid it in first place?
Given aliasing policy of Rust and the fact that panic
never returns not even the current user code in thread will see data corruption.
Summary of syntax
//TODO: EBNF for clarity?
Drawbacks
- This adds an entire kind of references.
- We may want a separate mechanisms for both partial initialization and
&move
\&own
references.
Rationale and alternatives
The feature serves 2 distinct needs: partial initialization and moving a value but not the memory. The benefit of this proposal is that it lets talk about both needs, but in the same time doesn't allow to forget about their deep connection. Moreover, here functions are coupled in one distinct mechanism, making it easier to learn it and to work with it.
The main alternative is to have two different features for both partial initialization and move references.
Prior art
-
"?Uninit types [exist today]. Also let’s talk about DerefMove" internals topic - the main source of inspiration.
-
Older move reference proposals - history of a feature.
Unresolved questions
- Should we allow moving an
Unpin
types out of a move reference? - Should we allow coercing
&mut T
to&move T!
and vice versa? - Is the way the
DerefMove
trait is defined here right? What's about another kinds of move references?
Future possibilities
I can't think of anything valuable yet.