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:
fully initialized, so the container we got the place from can be used again, after notify is called;
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?
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.