Heh, as the author of:
as well as:
I have thought quite a bit about this topic
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 FnOnce
s 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 toosince it would allow for
&own self
methods indyn Trait
s, thereby resolving the classic conundrum of "dyn
-safe trait with an owned receiver withoutalloc
/Box
(e.g.,no_std
environments).
Supporting Pin
ning is more trouble than it is worth.
Conceptually, a Pin<&own T>
cannot offer the Pin
ning 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, andPin::set(it, None)
onDrop
, 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 &'local
ly-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 aBox<T, Storage = Borrowed<'lt>>
of sorts (hence my originalStackBox
name in the crate; but since no actual heap-Box
ing 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 withPin<&own _>
, the macro "downgrades" its output toPin<&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 Trait
s or []
slices
With the Storage
basic API shown above, we could even start featuring returned dyn Trait
s:
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.