Disclaimer: This is still in the brainstorming phase, and I need to know if there are any major problems I missed. Please don't bikeshed syntax details for now
I've been thinking about placement new, and I realized that we "just" need to extend move analysis with two things:
struct MyStruct {
foo: String,
bar: [u64; 1024],
}
fn use_my_struct(_: MyStruct) {
todo!();
}
// THIS WORKS TODAY
let mut s = MyStruct { foo: String::new(), bar: [0; 1024] };
// move the fields out of the struct
drop(s.foo);
drop(s.bar);
// move new values into the struct
s.foo = String::from("hello world");
s.bar = [13; 1024];
use_my_struct(s);
Rust ensures that all fields are present when use_my_struct
is called. What's missing is a way to create a struct instance without its fields:
let mut s: MyStruct = empty!(); // built-in macro
// move new values into the struct
s.foo = String::from("hello world");
s.bar = [13; 1024];
use_my_struct(s);
And a way to initialize it from another function; this can be solved with what I call move references:
let mut s: MyStruct = empty!();
init_my_struct(&move s);
use_my_struct(s);
fn init_my_struct(s: &move MyStruct) {
s.foo = String::from("hello world");
s.bar = [13; 1024];
}
What's a move reference?
A move reference is similar to a mutable reference. However, it can be uninitialized at first. Once it has been fully initialized, it turns into a normal mutable reference.
Move references need to be linear types -- they can be neither copied, nor destroyed:
{
let mut s: MyStruct = empty!();
let r = &move s;
} // error! `r` goes out of scope here, but it can't be dropped
{
let mut s: MyStruct = empty!();
let mut r = &move s;
*r.foo = String::from("hello world");
*r.bar = [13; 1024];
} // this is ok, because `r` is fully initialized and turned into `&mut MyStruct`
This guarantees that functions that accept a move reference and do not return it need to initialize them. We can deduce from the lifetimes which move references are initialized by a function:
// initializes a, but not b
fn f<'a, 'b>(a: &'a move X, b: &'b move Y) -> &'b Z;
a
has to be initialized by the function f
, as that is the only way to drop it.
How would this work with Box
and Vec
?
Move references can be created from raw pointers, so it's possible to allocate memory on the heap and obtain a move reference to it.
To initialize a Box<MyStruct>
, we can add a function that accepts an initializer function:
impl<T> Box<T> {
fn init(initializer: impl FnOnce(&move T)) -> Self;
}
let boxed_struct = Box::init(|mut r| {
*r.foo = String::from("hello world");
*r.bar = [13; 1024];
});
Here, r
points to a newly created allocation on the heap. r
being a move reference guarantees that it is initialized within the closure. A similar approach is possible for Vec
:
impl<T> Vec<T> {
fn init(len: usize, initializer: impl FnMut(&move T, usize)) -> Self;
}
let struct_vec = Vec::init(20, |mut r, index| {
*r.foo = String::from("hello world");
*r.bar = [index as u64; 1024];
});
How can we enforce that move references aren't dropped?
The problem is that values of generic types can be dropped, like here:
fn drop<T>(value: T) {}
We need to make it impossible to call drop
with a move reference. This can be achieved with a Destroy
auto trait that is (like Sized
) implicitly added to all generics. Since T
does not have a ?Destroy
bound, we can't call drop
with linear types. Destroy
is implemented for all types except move references (and types containing move references).
How about types other than structs?
Tuples, structs and unions can be initialized by their fields. Enums can be initialized in place like this:
let mut opt: Option<MyStruct> = Some(empty!());
if let Some(value) = &move opt {
*r.foo = String::from("hello world");
*r.bar = [index as u64; 1024];
} else {
unreachable!()
}
Arrays and slices can not be initialized in a loop, since the compiler couldn't prove that every index was initialized. Instead, helper methods can be added:
impl<'a, T> &'a move [T] {
fn fill_uninit(self, value: T) where T: Clone;
fn fill_uninit_with(self, initializer: FnMut(&'a move T, usize));
}
Custom DSTs
A problem with custom DSTs is that you can't easily create them. Move references could alleviate this problem:
struct MySuperSlice {
info: u32,
data: [u8],
}
// passing `<&mut MySuperSlice as Pointee>::Metadata` as first parameter
let boxed_super_slice = Box::init_unsized(1024, |mut r| {
r.info = 30;
r.data.fill_uninit(17);
});
Panic safety
When an initializer panics, a value may be left in an uninitialized state. However, since &move T
does not implement UnwindSafe
, this should never be observable.