Place implicits for moves

Idea: allow smart pointers to be converted into places that the compiler can sparingly use.

Today we can see this only in Box: we can move out from it (that would be DerefMove), but we can also move into it;

Proposal

Place for any type T is such region of memory that is capable of holding a singular instance of T;

Place can be borrowed by current stack frame and in this case stack frame is responible for correct disposal of its content.

Borrowed place can be moved in and out (also partially, if T doesn't have Drop impl).

The Deref usecase:

I propose to say that when dereferencing of a Box(or any other container) actually happens we instead borrow the place with value.

Any type implementing the trait is considered a container owning its value;

The DerefPlace trait:

trait DerefPlace: Deref {
   /// Safety: caller must ensure that value that self points to is not used with pointer recieved from this func
   unsafe fn deref_place(*mut self) -> *mut Self::Target;
   /// Safety: caller must not call this if there's an init value inside of container
   unsafe fn drop_empty(&mut self);
   /// This is called by the compiler after borrow of place ends; 
   fn notify(&mut self) {};
}
  • It's formulated using the pointers to explicitly say we avoid copies here, but we transfer ownership. (m.b. use Unique<Self> ?);

  • The unsafe method drop_empty is called instead of Drop::drop in cases when container instance gets moved out and falls out of scope as such. (for box it'd be a call to dealloc)

  • notify is for cases like reactive cells, shader inputs and so on.

  • When borrow of place ends the value inside of it is either:

    1. fully initialized, so the container we got the place from can be used again, after notify is called;
    2. or not, in which case container is disposed.

The UX of this trait:

let b = Box::new(...); // or anything else that implements `DerefPlace`;

*b = ...; // here deref_place get's called, old value is dropped, new is written;
// at this line we turned b into a place, wrote to it, and turned it back;

let non_copy = *b.non_copy; // move out a `!Copy` field;
// but here we turn b into a place without closing it immediately

// from now on we `b` turned into a place until we again init `b.non_copy`
// once `b` is no longer a place, we notify it 
// and if we do not then `b` gets `DerefPlace::drop_empty`ed at the end of scope

GCE and NRVO

We can have the rules around places:

  • If we move into a deref (aka *some_box = make_val()) we pass the pointer from deref to make_val during optimizations
  • If inside of make_val there is a binding that is returned unconditionally it can be intialized directly in return place;
  • If inside of make_val exist a number of branches that each create a value to be returned and any pair of those values are not used simultaneously, then we can allocate any and all of them in return place.

While these are strictly optimizations, for proposed !Move types they are perhaps the only way to work with; maybe we need attributes to require these optimizations and fail if we cannot?

Would the Box impl be:

impl<T:?Sized> DerefPlace for Box<T>{
    unsafe fn deref_place(self:*mut Self)->*mut T{
        Self::into_inner(::core::ptr::read(self))
    }

    unsafe fn drop_empty(&mut self){
        let ptr = Self::into_inner(::core::ptr::read(self));
        alloc::alloc::dealloc(ptr.cast(), Layout::for_value(ptr.as_ref().unwrap()));
    }
}

Also, what does notify do?

impl is a bit different since box has inside both raw pointer and allocator reference, but general shape is right:

struct Box<T,A: Allocator>(*mut T, A);

impl<T, A: Allocator> DerefPlace for Box<T,A> {
   unsafe fn deref_place(*mut self) -> *mut T {
      self.0
   }
   unsafe fn drop_empty(&mut self) {self.1.dealloc(self.0,Layout::of::<T>())} 
   //or what was the method
}

notify is callback that is fired after the contents has been possibly rewritten, and is accessible (which is not the case for deref_place method by return of which container is not allowed to intrude in the memory (it's borrowed))

  • it may be used for good handles for remote memory for example, where actual write is a different operation that mere buffer update; notify can flush buffer using rdma operations;

  • also may be used for reactive cells, which use notify to schedule updates;

if these operations are cheap enough - may be this method is worth including.

I’m not sure a notify method is really compatible with the way things like Box currently operate.

For example… say, I have a Box<(Foo, Bar)>, (neither type Copy), then move out the Foo and the Bar, and conditionally move back in either:

struct Foo;
struct Bar;

fn condition() -> bool {
    rand::random()
}

fn demo() {
    let mut x = Box::new((Foo, Bar));
    drop(x.0);
    drop(x.1);
    if condition() {
        x.0 = Foo;
    }
    if condition() {
        x.1 = Bar;
    }
    // here
}

should the compiler now be asked to insert “here” some code that, at run-time, checks the drop flags and determines whether or not x is fully initialized again? Or should these things only be done depending on static analysis (this is a point where at least your proposal is quite under-specified).

In any case, since it’s optional to implement anyways, this notify method seems like the perfect thing to be left out of such a proposal since it could equally well be proposed separately at a later time. Always think of the minimal viable product language feature, especially if your envisioned extensions are already addressing separate / additional concerns and are backwards compatible, anyways.


Also, as an unrelated note, DerefPlace should be an unsafe trait.

2 Likes

Statical only, since we don't have anything to do if x's not initialized;

I prefer to be pessimistic about such conditions, so at here from your example x is gonna be uninitialized (even if both moves has happened).

This also means that starting from mark the x is uninitialized:

let mut x = Box::new((Foo, Bar));
if condition() {
    drop(x.0);
}
if condition() {
    drop(x.1);
}
// mark

The exception is when one of branches that move out diverge - what is moved out on it is not invalidated.

So the answer is - No.

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.