A sketch for `&move` semantics

Heh, as the author of:

as well as:

I have thought quite a bit about this topic :smile:

Naming-wise, own-ing references is better than move references

Indeed, the whole point of the references is not to move stuff. The "responsibility to drop" is actually more often called ownership. So, to keep things clearer, I'll keep referring to them as &own T references. Whilst move being a keyword seems convenient at first glance (and the reason back in the day I too called them &move references), I have since changed my mind: that naming confuses too many people ("something by reference is not moved").

  • I think we could afford a contextual keyword here, with another edition; incidentally it would remove the need to disambiguate &move ||, which is not that niche, since a good motivation for owned references is that of constructing &own dyn FnOnces and the like (see below).

Semantics are already fleshed out and exist in the aforementioned stackbox crate.

And they happen to be quite simple!

Click here to see the Rust code

&own value would be equivalent (modulo lifetime extension) to doing:

Storage::SLOT.init(value)

with:

pub struct Storage<T>(MaybeUninit<T>);

impl<T> Storage<T> {
    pub const SLOT: Self = Self(MaybeUninit::uninit());

    pub fn init(self: &mut Storage<T>, value: T) -> Own<'_, T> {
        Own(self.0.write(value))
    }
}

which would result in a &'local own T, or Own<'local, T> in user-library parlance, with:

pub struct Own<'storage, T: ?Sized>(&'storage mut T);
// and all the good `{Coerce,}Unsize`  impls for nice unsizing.

impl<T : ?Sized> Deref{,Mut} for Own<'_, T> {
    type Target = T;

    ...
}

impl<T : ?Sized> Drop for Own<'_, T> {
    fn drop(&mut self) {
        unsafe { <*mut T>::drop_in_place(self.0) }
    }
}

And that's it.


The missing part are thus ergonomics: creating a &own reference with library code is currently cumbersome (look at all the offered constructors in stackbox!), especially related to lifetime extension.

  • Lifetime extension would be key for this to be ergonomic

  • Being able to use &own self receivers too

    since it would allow for &own self methods in dyn Traits, thereby resolving the classic conundrum of "dyn-safe trait with an owned receiver without alloc/Box (e.g., no_std environments).

Supporting Pinning is more trouble than it is worth.

Conceptually, a Pin<&own T> cannot offer the Pinning guarantees, since it does not own the T's backing allocation (it only owns T's drop glue, so, if forgotten, the pointee will be deallocated without its drop glue being run, to summarize what has already been mentioned in this thread).

So, while maybe an effort could be made to support it; we'd be "swimming against the tide", of sorts, so it does not seem wise to start with that.

  • "Remote drop flags" would probably help tackle this design space, but it does not seem to be worth focusing on this for a first implementation: as mentioned, scoped APIs (or macros?) could let third-party libraries polyfill this design space initially; there is no need to rush language sugar for this initially. (Moreover, a Pin<&mut Option<T>> wrapper which would auto-unwrap, and Pin::set(it, None) on Drop, seems quite equivalent to this suggested language magic, so the magic seems unwarranted?)

I suspect drop flags may lead to a bunch of design questions, and thus an impression of lack of clarity around the design, which is very much not the case, as I've shown in the aforementioned code.

Benefits of &own T

It "fits the picture"

First and foremost, it would fill the missing third mode for references:

Semantics for T For the backing allocation
&T Shared access Borrowed
&mut T Exclusive access Borrowed
&own T Owned access
(drop responsibility)
Borrowed

That way the troΓ―ka/trinity/trifecta triumvirate of Rust design would finally apply to the &-indirection mode of references.

Some people, back in the day, complained about this point, because of a beginner mixup between ownership and being : 'static. You can be : 'static without being responsible of any drop glue (e.g., &'static ... references), and you can be 'lt-infected while being responsible of drop glue (e.g., BoxFuture<'lt, ...>). So the fact we have a &'locally-lived reference with drop glue ought not to be surprising (as a matter of fact, there is the tangentially related dyn* Trait + 'local design which runs into the same paradigm).

  • In fact, a Own<'lt, T> can be conceptualized with the storage API as a Box<T, Storage = Borrowed<'lt>> of sorts (hence my original StackBox name in the crate; but since no actual heap-Boxing occurs, I find the "stronger &mut" naming to better fit the picture than talking about boxes).

  • we already have one instance of this concept in the standard library: the pin! macro consumes ownership of the given value, and returns a temporary borrow to it (it's just that because of the aforementioned issues with Pin<&own _>, the macro "downgrades" its output to Pin<&mut _> for soundness).

It supersedes, with less magic / more honest and transparent semantics, unsized_fn_params.

That is, it trivially solves the Box<dyn FnOnce> : FnOnce ? and any other such occurrences wanting to take a dyn Trait "by value", ideally in an allocation-agnostic way:

/// This trait is object/`dyn`-safe
trait DynSafe {
    // No need for unsized_fn_params, thanks to `&own ?Sized` references:
    fn example(&self, f: &own dyn FnOnce()) {
        f(arg)
    }
}
// this is an example of `dyn`-safe polymorphism over an ownership-based trait.
  • For instance, this supersedes the usual &mut Option<FnOnce()> dance that is so pervasively polyfilling this API gap in several occurrences.

This does not exclude unsized_fn_params sugar, afterwards, if deemed ergonomic enough to warrant all the extra language magic, from being added; but at that point it would amount to:

fn example(f: dyn FnOnce()) {
    let g = f; // what does this do??
    g()
}

example(|| { ... })

being sugar for:

fn example(f: &own dyn FnOnce()) {
    let g = f; // Ok, it just "copies the ptr" / moves the owning pointer.
    g();
}

example(&own || { ... })

It unlocks "Future Possibilities"

Returning dyn Traits or [] slices

With the Storage basic API shown above, we could even start featuring returned dyn Traits:

type ActualFn = impl Sized;

fn create(storage: &mut Storage<ActualFn>)
  -> &own dyn FnOnce()
{
    storage.init(|| { ... })
}
type ActualArray = impl Sized();

fn create(storage: &mut Storage<ActualArray>)
  -> &own [String]
{
    storage.init([
        String::from("hello"),
        String::from("world"),
    ])
}
  • which is something unsized_fn_params can't even dream of, since it hides all the 'storage semantics from the picture;

  • which could help with the -> impl Trait in Trait effort;

In-place initialization

Here &'storage out T, coupled with generative API would allow writing in-place constructors, in a way that can perfectly be unwind-safe.

Basically this:

but with my own correction that it can be made sound (precisely by having each in-place initialization yield a Own<'storage, Field, Brand<'_>> token): see GitHub - moulins/tinit: An experiment for safe & composable in-place initialization in Rust.

Move constructors

Probably from the previous point and adding Pin into the mix, c.f. the moveit - Rust crate.

15 Likes