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
/&move
references 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
/&own
references, 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 memcpy
s) around!!.
- The reason to be calling them
&move
thus stems from "move semantics" being kind of a way of saying "ownership semantics", and from the practical aspect ofmove
already being a keyword, which makes&move
a retro-compatible language addition. But since this aspect of the discussion is out of topic here, I'll stick to&own
from 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 T
up 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
Box
is thatBox
is 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
Allocator
API out of a&'storage mut MaybeUninit<T>
(we could call itStackAllocator<'storage>
), and then saying&'storage own T
has 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 Trait
s (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 Trait
s 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 Trait
s"), 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 Trait
s 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
memcpy
if the size of the type is big (a "subcase", we could say, of supporting non-Sized
types); -
not using a
Box
.