Idea: Limited custom move semantics through explicitly specified relocations
Please take my ideas lightly as these ‘ideas’ aren’t particularly thought through. I simply want to seek feedback. Also this post contains pseudocode! Not valid rust code.
This idea are some thought collections regarding the difficulties and solutions of self-referential structs. In particular, this idea is based on the fact that we could specify our own move semantics but in a very limited way (just to support self-referential structs).
To give you some context, please see this conversation between redditors discussing a similar idea:
Tl;dr: The idea is to support a custom trait which when implemented applies custom move semantics where you can ‘reinitialize’ fields which contain self-ref-lifetimes.
But this idea is flawed because you couldn’t possibly know where the references were pointing to. Also re-initializing heap space would be problematic in the same way.
This is where my idea comes in:
We may introduce two new entities: A trait called Relocatable
and a internal struct called Relocation
.
The idea is basically make relocatable references by creating them on-creation through a closure. The following pseudo-code shows the core-idea behind this approach.
// This trait must be implemented on structs which contain self-ref-lifetimes
trait RefMove {
// This method is called whenever a move has occured.
// Object moves -> object is memcpy-ed (default move semantics applied) -> move() is called
// This method shall relocate each field which contains self-ref-lifetimes
unsafe fn move(&mut self);
}
// This trait is implemented by entities which can be relocated, based on an 'new_obj' with a generic type O.
trait Relocatable<O> {
unsafe fn relocate(&mut self, new_obj: &O);
}
// Magically generated struct for self-ref-lifetimes
// ----
// This is a Relocation which is basically a relocatable reference pointer, the idea is to to have a relocator which can generate a new reference based on the base object.
struct Relocation<T, O> {
cached_ref: *const T,
relocator: Box<Fn(&O) -> &T>
}
impl<T, O> Relocation<T, O> {
unsafe fn to_ref<'a>(&self) -> &'a T {
std::mem::transmute::<*const T, &'a T>(self.cached_ref)
}
}
impl<T, O> Relocatable<O> for Relocation<T, O> {
// This performs the the actual relocation by calling the relocator with the new base object.
unsafe fn relocate(&mut self, new_obj: &O) {
self.cached_ref = (self.relocator)(new_obj) as *const T;
}
}
// ----
// The Relocatable trait may be additionaly implemented by Vec, Option, ... :
impl<O, R: Relocatable<O>> Relocatable<O> for Vec<R> {
unsafe fn relocate(&mut self, new_obj: &O) {
for elem in self.iter_mut() {
elem.relocate(new_obj);
}
}
}
// ----
struct Data {
a: u32,
b: u32
}
struct Foo {
data: Data,
// Internally self-ref-lifetimes are represented by a compiler-generated 'Relocation<X, Y>'.
// Where X is the reference type and Y is the enclosed type.
a_or_b: &'data u32, // Actually: Relocation<u32, Foo>
heap: Vec<&'data u32> // Actually: Vec<Relocation<u32, Foo>>
}
impl RefMove for Foo {
unsafe fn move(&mut self) {
// This is actually invalid rust. But you get the idea.
self.a_or_b.relocate(self);
self.heap.relocate(self);
}
}
impl Foo {
fn new(c: bool) -> Self {
let mut ret = Self {
data: Data { a: 0, b: 1 },
heap: Vec::new()
}
// relocate may be a library function which creates a 'Relocation':
// fn relocate<'a, O, T>(obj: &O, relocator: impl Fn(&O) -> &'a T) -> &'a T
// ^ This is probably wrong but I simply want to express that the closure
// returns a reference with a lifetime which derived from the &ret reference
ret.heap.push(relocate(&ret, move |obj| {
if c {
&obj.data.a
} else {
&obj.data.b
}
}));
ret
}
}