Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

RFC - Add an owning "borrowed" pointer type &move #1617

Closed
wants to merge 6 commits into from
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
103 changes: 103 additions & 0 deletions text/0000-move-pointer.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
- Feature Name: `move_pointer`
- Start Date: 2016-05-12
- RFC PR: (leave this empty)
- Rust Issue: (leave this empty)

# Summary
[summary]: #summary

Introduce a new pointer type `&move` that logically owns the pointed-to data, but does not control the backing memory. Also introduces the DerefMove trait to provide access to such a pointer.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What does 'logically owns' mean?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That the pointer owns the pointed-to data, and is responsible for calling the destructor on it (and can be moved out of)


# Motivation
[motivation]: #motivation

This provides an elegant solution to passing DSTs by value, allowing `Box<FnOnce>` to work. It also will allow other usecases where trait methods should take self by "value".
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The RFC doesn’t seem to actually specify how Box<FnOnce> would be made to work through this RFC—would the trait be changed to take &move self for FnOnce::call_once or what?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's the idea. I don't think there's any problems with that (as the trait is still unstable). I can add it as an appendix to the RFC document.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The compiler can implement by-value T: ?Sized as &move T, there are no fundamental issues there AFAIK. The trouble is letting library types actually deref to a &move T, which this RFC addresses.


# Detailed design
[design]: #detailed-design

- Add a new pointer type `&move T`
- This pointer will get the same lifetime rules as `&` and `&mut` (such that it cannot outlive the allocation)
- but, it will allow the value to become invalid before the allocation does.
- Deref coercions as per RFC #241 apply.
- Variables of type `&move T` will only allow mutating `T` if they themselves are `mut`
- Add a new unary operation `&move <value>`. With a special case such that `&move |x| x` parses as a move closure, requiring parentheses to parse as an owned pointer to a closure.
- This precedence can be implemented by ignoring the `move` when parsing unary operators if it is followed by a `|` or `||` token.
- Add a new operator trait `DerefMove` to allow smart pointer types to return owned pointers to their contained data.
- A type that implements `DerefMove` cannot implement `Drop` (as `DerefMove` provides equivalent functinality)
Copy link
Contributor

@durka durka May 17, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Arc and Rc implement Drop in order to decrement the refcount and possibly deallocate the contained object. Presumably they will also want to implement DerefMove. How will this work? Will drop(arc); cause DerefMove::deallocate(&mut arc); to be called?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DerefMove makes no sense for shared ownership IMO.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does DerefMove provide equivalent functionality to Drop?

Imagine that Box<T> implemented DerefMove. Then it couldn't implement Drop.

fn main() {
let x = Box::new(0);
// I never moved out of the box, so it gets leaked here?
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It provides the same functionality as Drop. See the example in the RFC.... that said, I'm not sure how dropping of non-moved members would be handled.


```rust
trait DerefMove: DerefMut
{
/// Return an owned pointer to inner data
fn deref_move(&mut self) -> &move Self::Target;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I could very well be wrong, but shouldn’t this take &move self? Wouldn’t this otherwise allow things like the following:

let x = Box::new(...);
{
    drop(*x.deref_move());
}
// use x (even though its contents have been destroyed!)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point. I suppose making it unsafe is the cop-out option (which semi-fits, because you'd need unsafe code to implement it).

It can't really be &own, because that "moves" the container into the deref_move method, leading to it being dropped before the pointer you return.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the crux of the problem. We need typestate and an associated "empty container" type to do this correctly.

Copy link
Contributor

@Ericson2314 Ericson2314 May 17, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm I have an idea where &move Uninit<T> == &out T. I am going to fork the RFC to write this. edit nope typestate + move + drop by value + ... would take forever to write.

Copy link
Contributor

@Ericson2314 Ericson2314 May 18, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A teaser:

"type state" in my parlance would allow for something like:

fn foo(arg: {'lifetime, BeforeType, DuringType, AfterType}, ...) -> ...;

e.g.

fn take_ref<'a, T>(self: {'a, T, MutBorrowed<T>, T}) -> &'a mut T;
fn take_mut_ref<'a, T>(self: {'a, T, Aliased<T>, T}) -> &'a T;

Now suppose there is some Uninit<T>, which has no destructor and can only be written too (in which case it becomes a T).

fn move_in<'unused, T>(self: {'unused, Uninit<T>, Whatever, T}, T) -> ();
fn move_out<'unused, T>(self: {'unused, T, Whatever, Uninit<T>}) -> T;

Now suppose that T::Toggle = Uninit<T>, and Uninit<T>::Toggle = T, so we have a way to go there and back:

fn take_move_ref<'a, T>(self: {'a, T, MutBorrowed<T>, T::Toggle}) -> &'a move T;

// Type App
fn take_move_ref::<T::Toggle>: fn (self: {'a, T::Toggle, MutBorrowed<T::Toggle>, T::Toggle::Toggle}) -> &'a move T::Toggle;
// Eliminate ::Toggle::Toggle
fn take_move_ref::<T::Toggle>: fn (self: {'a, T::Toggle, MutBorrowed<T::Toggle>, T}) -> &'a move T::Toggle;

type OutPtr<'a, T> = & 'a move T::Toggle;

And low and behold we get out-pointers for free! (spelling credit: @eddyb)

Copy link
Contributor

@Ericson2314 Ericson2314 May 18, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For re-borrowing:

fn reborrow_mut<'a: 'b, T>(self: {'b, &'a mut T, &'a mut MutBorrowed<T>, &'a mut T}) -> &'b mut T;
fn reborrow_move<'a: 'b, T>(self: {'b, &'a move T, &'a move MutBorrowed<T>, &'a move T::Toggle}) -> &'b move T;

and for the original motivation:

struct Box<T>(&'static move T);
type DeadBox<T>(&'static move Unwrap<T>);

fn straight_outta_box<'a, T>(self: {'a , Box<T>, Box<MutBorrowed<T>>, DeadBox<T>}) -> &'a move T {
    reborrow_move(self.0)
}

Copy link
Contributor

@Ericson2314 Ericson2314 May 18, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And finally, dropping, which is very easy:

impl<T> Drop for Box<T>
{
    fn drop_interior(arg: &move Interior<Box<T>>) {
        let x: &move T = move_out(arg).0;
        // arg: OutPtr<Box<T>> // no-op to drop
        drop(reborrow_move(x));
        // x: OutPtr<T> // we can still use it (no-op to drop, too)
        let ptr: *u8 = x as _;
        unsafe {
            heap::deallocate(ptr, mem::size_of::<T>(), mem::align_of::<T>());
        }
    }
}

Dropping Uninit<T> is a no-op, so we don't need to worry about dropping DeadBoxes at all!

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@glaebhoerl pinging you because of our shared interest in these things.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, in hindsight the &move Unint<T> is an outpointer thing doesn't hold up, because out-pointers are linear / can only be consumed through writes, but &move Unint<T> needs to be droppable after a move-out, like in my Box example.

Otherwise I do think this stuff works, though granted its a major addition to the language.

/// Equivalent to `Drop::drop` except that the destructor for `Self::Target` is not called
fn deallocate(&mut self);
}
```

When an owned pointer is dropped (without having been moved out of), the destructor for the contained data is called (unlike `&mut` pointers, which are just borrows). The backing memory for this pointer is not freed until a point after the `&move` is dropped (likely either at the end of the statement, or at the end of the owning block).

For example, the following code moves out of a `Box<T>` into a `&move T` and passes it to a function
```rust
fn takes_move(val: &move SomeStruct) {
// ...
}
fn main() {
let val = Box::new( SomeStruct::new() );
takes_move( &move val );
println!("Hello");
}
```
This becomes the following operations
```rust
fn main() {
let val = Box::new( SomeStruct::new() );
takes_move( DerefMove::deref_move(&mut val) );
DerefMove::deallocate(&mut val);
println!("Hello");
}
```


# Drawbacks
[drawbacks]: #drawbacks

- Adding a new pointer type to the language is a large change

# Alternatives
[alternatives]: #alternatives

- Previous discussions have used `&own` as the pointer type
- This name is far closer to the actual nature of the pointer.
- But, since `own` is not a reserved word, such a change would be breaking.

# Unresolved questions
[unresolved]: #unresolved-questions

- Potential interactions of what happens when a `&move` is stored.
- If a `&move` is stored in the current scope, when is the original storage freed?
- If a `&move` isn't stored, is the storage freed right then, or when it would have otherwise gone out of scope?

- `IndexMove` trait to handle moving out of collection types in a similar way to `DerefMove`
- Should (can?) the `box` destructuring pattern be implemented using `DerefMove`?
- This would be a larger RFC specifying the `box` pattern using the `Deref` family of traits


# Appendix: Implementations of `DerefMove`
```rust
impl<T: ?Sized> DerefMove for Box<T>
{
fn deref_move(&mut self) -> &move Self::Target {
unsafe {
&move *(self.0)
}
}
fn deallocate(&mut self) {
unsafe {
heap::deallocate(self.0, mem::size_of_val(&*self.0), mem::align_of_val(&*self.0));
}
}
}
```