Continuing the discussion from Problems with Attempts at Type Erasure using Boxes:
There are two things to observe when talking about &move references:
-
An addition to the language / grammar / syntax, so as to improve otherwise quite cumbersome ergonomics. There have been discussions about
&own/&movereferences in some github issues and on IRLO about these additions (FIXME: add links), and indeed discussing about language changes does not fit here on URLO, and we might end up rehearsing what was said in such places, so if somebody has something to mention regarding this topic / aspect of&move/&ownreferences, then I suggest that they do so there and not here. -
Certain semantics / an API. In the remainder of my post (and I hope, this thread), I (we) will be talking about this aspect.
The semantics of &move / &own references
When dicussing about this, calling them &own rather than &move may already make the whole thing a bit clearer: the very point of &own references is about not having to move stuff (in the sense of performing memcpys) around!!.
- The reason to be calling them
&movethus stems from "move semantics" being kind of a way of saying "ownership semantics", and from the practical aspect ofmovealready being a keyword, which makes&movea retro-compatible language addition. But since this aspect of the discussion is out of topic here, I'll stick to&ownfrom now on.
So, the main thing to observe is that the semantics of &'_ own T references would be very similar to those of a Box<T>, in the sense that it would be a pointer which:
-
points to a valid and well-aligned instance of type
T;
in a unique / unaliased manner;- (behaving like a
&'_ mut Tup until this point)
- (behaving like a
-
calls
ptr::drop_in_place::<T>(), i.e., drops the pointeeT, when it is itself (the owning pointer) dropped.
All in all, it's a an owning pointer to a T / a pointer which owns its pointee / the instance of type T it points to.
- The reason for this design very similar to
Boxis thatBoxis not always available (::alloc-less crates such as#![no_std]embedded environments) or may be deemed too expensive (performance-critical hot functions) to use when what the caller wanted was "simply" a basic layer of indirection (such asBox) but without heap-allocating (thus such as&mut) but which does retain ownership semantics (such as … err …&own T).
Since it does not directly allocate the storage for T, a &'storage own T, contrary to a Box<T>, does not free that storage either.
That is, whilst a Box<T>, when dropped, drops the T instance and then frees the backing storage / memory, an &'_ own T, when dropped, only drops the T instance, since it was only borrowing the backing storage. Hence why it is still tied to a lifetime '_, which is the span of the borrow over that backing storage:
#[language_sugar = &'a own T]
pub
struct RefOwn<'a, T : ?Sized> /* = */ (
&'a mut T,
);
/// Or a full new-type
type Slot<T> = MaybeUninit<T>;
pub
fn new_in<'storage, T> (
storage: &'storage mut Slot<T>,
value: T,
) -> &'storage own T
{
let ptr: &'storage mut T = storage.write(value);
impl<'storage, T : ?Sized> Drop for RefOwn<'storage, T> {
fn drop (self: &'_ mut RefOwn<'storage, T>)
{
unsafe { <*mut T>:::drop_in_place(&mut *self.0) }
}
}
RefOwn::<'storage, T>(ptr)
}
- Note that one possible way to achieve all this would be by deriving some dummy
AllocatorAPI out of a&'storage mut MaybeUninit<T>(we could call itStackAllocator<'storage>), and then saying&'storage own Thas the semantics ofBox<T, StackAllocator<'storage>>. This maybe would be doable, but having a "false" allocator (e.g.,free()would be kind of a no-op; and the allocator would work as singleton non-Copy/ non-borrowable instance) seems: not everything must be expressed exactly in terms ofBox!
And, indeed, this whole API has already been implemented in a third-party library (disclaimer, written by me), as the StackBox<'storage, T> abstraction:

stackbox
`&own`ing references in stable Rust - no_std-friendly Box
Why?
I believe the answer is very similar to why one would be using Box to begin with: because indirection adds a whole new set of type-erasure-based capabilities, such as Unsize coercions:
-
array-to-slice (erasure of the statically-known length):
PtrTo<[T; N]>can coerce toPtrTo<[T]>, -
dyn Traits (erasure of the statically-known method dispatch):
PtrTo<impl Trait>can coerce toPtrTo<dyn Trait>
Notice how APIs dealing with the left-hand-side of these coercions (arrays, polymorphism over concrete trait implementors) need to do so with generics, which leads to code bloat and, if used as methods of another Trait, to making Trait not be dyn-safe / dyn-compatible, or to making those methods not be callable on a dyn Trait:
-
#[dyn_safe(true)] // Error, `.zeroize()` method is generic and thus not dyn-safe trait Zeroize { fn zeroize<const N: usize>(&self, buf: &mut [u8; N]); }
-
#[dyn_safe(true)] // OK trait Zeroize { - fn zeroize<const N: usize>(&self, buf: &mut [u8; N]); + fn zeroize (&self, buf: &mut [u8 ]); }
As well as:
-
#[dyn_safe(true)] // Error, `.scoped_block_on()` method is generic and thus not dyn-safe trait ScopedExecutor<'scope> { fn scoped_block_on ( self: &'_ Self, fut: impl 'scope + Future<Output = ()>, ); }
-
#[dyn_safe(true)] // OK trait ScopedExecutor<'scope> { fn scoped_block_on ( self: &'_ Self, - fut: impl 'scope + Future<Output = ()>, + fut: Pin<&'scope mut dyn Future<Output = ()>>, ); }
While &mut is still pretty handy to enable these coercions, it does have the issue of not allowing owning APIs / interacting with owned unsized types such as dyn Traits or slices without ::alloc (e.g., try to write a dyn-safe trait with a drop_all method which drops all the elems of an input array or slice, without using allocations such as Vec: the least bad thing to do is to take a slice of [Option<T>] and .take() them all, or to take a &mut dyn Iterator<Item = T>, and both things suffer from there occuring n dynamic branches, where n is the length of the slice).
// Basic example
#[dyn_safe(true)]
trait Runner {
fn run (&mut self, f: &move dyn FnOnce());
}
While some of these examples are already handled by another possible feature of the language, unsized_fn_params, which is basically "magic implicit &move on function inputs so as to avoid input owned dyn Traits"), the issue with something being magical and, for instance, preventing access to the lifetime of the &move handle is that it prevents further tweaks and optimisations, such as returning owned dyn Traits and other unsized types:
fn mk_dyn_fn_once<'s> (
// imaginary short-hand for
// an inlined existential type
// vvvvvvvvvvvvvvvvvvvvvvvvv
storage: &'s mut Slot<#[existential] impl Sized>,
) -> &'s move dyn FnOnce()
{
new_in(storage, move || { … })
}
so that a caller that wants to stack-allocate themselves would just:
let mut storage = Slot::uninit();
let f = mk_dyn_fn_once(&mut storage);
// They can drop / call `f` until the point where `storage` is dropped.
// They cannot, for instance, return `f`.
and for someone still wanting to "return" the f to yet another caller, they'd just pass along the requirement of providing the storage:
fn middleware<'s> (storage: &'s mut Slot<#[existential] impl Sized>)
-> Option<&'s move dyn FnOnce()>
{
let f = mk_dyn_fn_once(storage);
if some_cond() { f(); None } else { Some(f) }
}
And so on and so forth.
Another nifty usage of this &move would be for non extern "Rust" functions that would like to avoid unnecessary copies of big Sized types that they pass by value: without &own pointers, one cannot have all of the three following things:
-
ownership / move semantics for the input;
-
passing the value by pointer / through indirection to avoid a
memcpyif the size of the type is big (a "subcase", we could say, of supporting non-Sizedtypes); -
not using a
Box.