Join GitHub today
GitHub is home to over 31 million developers working together to host and review code, manage projects, and build software together.
Sign upPropose `Interior<T>` data-type, to allow moves out of the dropped value during the drop hook. #1180
Conversation
aidancully
referenced this pull request
Jun 29, 2015
Closed
`ManuallyDrop` type gives precise control of dtors of inline data. #197
alexcrichton
added
the
T-lang
label
Jun 29, 2015
eefriedman
reviewed
Jun 30, 2015
| ## `impl DropValue for Interior<T>` | ||
|
|
||
| We do not prohibit `impl DropValue for Interior<T>`: it should just | ||
| work in the natural way. |
This comment has been minimized.
This comment has been minimized.
eefriedman
Jun 30, 2015
Contributor
Is this proposing to add a special case to the orphan rules? (Normally, you can't implement traits defined in another crate for types defined in another crate.)
This comment has been minimized.
This comment has been minimized.
aidancully
Jul 1, 2015
Author
The idea on this line is that T is defined in a local crate, so I think impl DropValue for Interior<T> is valid per orphan rules, but I'll double-check.
This comment has been minimized.
This comment has been minimized.
eddyb
Jul 1, 2015
Member
Drop has special rules so you can't have partial impls for generic types:
struct Foo<T>(T);
// error: Implementations of Drop cannot be specialized [E0366]
impl Drop for Foo<()> {
fn drop(&mut self) {}
}
This comment has been minimized.
This comment has been minimized.
aidancully
Jul 4, 2015
Author
Right, I've just read the discussion around that... You're right, I don't want to change this behavior. I'll amend the RFC.
eefriedman
reviewed
Jun 30, 2015
| this means that `Interior<T>` does not implement `Drop`, which means | ||
| it is (by default) valid to partially or fully deconstruct an | ||
| `Interior<T>` instance (even where it would not be valid to | ||
| deconstruct an instance of `T`). |
This comment has been minimized.
This comment has been minimized.
eefriedman
Jun 30, 2015
Contributor
You probably want to explicitly note how private fields of T are handled.
This comment has been minimized.
This comment has been minimized.
aidancully
Jul 1, 2015
Author
They would continue to be private to the module where T was defined, but I can note that explicitly.
eefriedman
reviewed
Jun 30, 2015
| fn as_interior<T>(t: &T) -> &Interior<T>; | ||
| fn as_interior_mut<T>(t: &mut T) -> &mut Interior<T>; | ||
| fn of_interior<T>(t: &Interior<T>) -> &T; | ||
| fn of_interior_mut<T>(t: &mut Interior<T>) -> &mut T; |
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
eefriedman
Jun 30, 2015
Contributor
Actually, I'm not completely sure whether these need to be unsafe. into_interior and from_interior seem suspicious.
This comment has been minimized.
This comment has been minimized.
aidancully
Jul 1, 2015
Author
The way I see it, the safety of these functions should match the safety of mem::forget, which has recently been made safe. But I'll add a note to that effect in the text.
This comment has been minimized.
This comment has been minimized.
eefriedman
Jul 1, 2015
Contributor
Consider an example like this:
struct MaybeCrashOnDrop { c: bool }
impl Drop for MaybeCrashOnDrop {
fn drop(&mut self) {
if self.c {
unsafe { *(1 as *mut u8) = 0 }
}
}
}
pub struct InteriorUnsafe { m: MaybeCrashOnDrop }
impl InteriorUnsafe {
pub fn new() -> InteriorUnsafe {
InteriorUnsafe { m: MaybeCrashOnDrop{ c: true } }
}
}
impl Drop for InteriorUnsafe {
fn drop(&mut self) {
self.m.c = false;
}
}Currently, safe code can't break safety rules using an InteriorUnsafe, but it could if into_interior were considered safe, I think?
This comment has been minimized.
This comment has been minimized.
aidancully
Jul 1, 2015
Author
I'm not the best person to make this argument, but my understanding of Rust's current definition of "unsafe" is that it refers specifically to memory-unsafety - that is, in the ability to access invalid memory. As your example shows, into_interior can provide the ability to violate API invariants, in the same way that mem::forget can (see the thread::scoped discussion, rust-lang/rust#24292, and the mem::forget discussion, #1066). It is a foot-gun, just like mem::forget is. But it does NOT do anything memory unsafe, and it does not cause unsafe behavior (though it can, as you show, cause an invariant to be violated in such a way that it results in unsafe behavior). I'm not persuaded that these functions are unsafe.
This comment has been minimized.
This comment has been minimized.
eefriedman
Jul 3, 2015
Contributor
mem::forget is specifically documented as something safe code is allowed to do, into_interior is not, and changing that isn't backwards-compatible.
Safe code staying safe depends on unsafe code maintaining its invariants. One invariant that unsafe code can currently depend on is that the drop implementation for a struct runs before the drop implementations of its members. Breaking that invariant breaks backward-compatibility: a library whose invariants can't be broken with safe code in 1.0 can have its invariants broken in 1.x.
I think you could get around the safety issue if the members of an Interior<T> were also of type Interior<T>. Of course, that leads to ergonomic issues because you can accidentally leak members of a struct.
This comment has been minimized.
This comment has been minimized.
eddyb
Jul 4, 2015
Member
I have to agree with @eefriedman here, being able to break the invariants of a type you didn't define, from safe code, is a no-no.
For some reason I thought T -> Interior<T> was safe, and it might be in most existing Drop implementations, but not all of them: some put their fields in a state that would make the fields' destructors noops, or at least not memory-unsafe.
This comment has been minimized.
This comment has been minimized.
aidancully
Jul 4, 2015
Author
I base the argument that into_interior is safe around a lot of the discussion I saw in the mem::forget discussion. Given that discussion, if, hypothetically, this proposal had been made and accepted before Rust 1.0 shipped, would this function have been called safe or unsafe? Considering comments like this one, it's hard for me to believe that into_interior would have been made unsafe.
I think what's being described is not so much an "unsafety" issue is actually a backwards-compatibility issue... (And, note, this same back-compat issue would also exist under the consuming-destructuring alternative.) The only consistent resolution I see is to disallow into_interior for Drop types. Which means that both Drop and into_interior need more special-case logic in the language than I had originally thought...
This comment has been minimized.
This comment has been minimized.
eefriedman
Jul 4, 2015
Contributor
Yes, you could think of it as a backwards-compatibility issue.
Under the consuming-destructuring proposal, we would probably only allow destructuring Drop types if the type is defined in the same crate as the destructuring use, unless someone has a better idea for a rule. A bit restrictive, but it dodges the backwards-compatibility concerns.
This comment has been minimized.
This comment has been minimized.
|
Might be worth explicitly describing the shim the compiler has to generate for trait vtables. (Consider the code necessary to implement Overall, this seems like a solid proposal. |
This comment has been minimized.
This comment has been minimized.
|
cc me |
This comment has been minimized.
This comment has been minimized.
|
Thanks for proposing this @aidancully, especially making it backwards compatible! |
Ericson2314
reviewed
Jul 3, 2015
| println!("drop_bar"); | ||
| // "this" is consumed at function exit, so the compiler | ||
| // will generate the following code: | ||
| // DropValue::<Foo>::drop_value(into_interior(this.0)); |
This comment has been minimized.
This comment has been minimized.
Ericson2314
Jul 3, 2015
Contributor
Is it possible to do partial moves like this? Otherwise do:
let Bar(x, y) = this;
DropValue::<Foo>::drop_value(into_interior(x));
DropValue::<Foo>::drop_value(into_interior(y));
This comment has been minimized.
This comment has been minimized.
aidancully
Jul 3, 2015
Author
In the proposal, it's possible to do this so long as DropValue isn't defined for Interior<Foo>: partial moves are allowed when type doesn't implement DropValue. In the example, DropValue is implemented for Bar, but not for Interior<Bar>; so a partial move out of Interior<Bar> is allowed. That said, the proposal needs better discussion of partial moves.
This comment has been minimized.
This comment has been minimized.
eddyb
Jul 3, 2015
Member
Interior<T> cannot implement DropValue if the same sanity checks we do for Drop are kept (and I don't see why that should be possible).
This comment has been minimized.
This comment has been minimized.
Ericson2314
Jul 3, 2015
Contributor
As I said like in our discussion long below, I still prefer tying destructuring and functional update to whether the type has private fields, rather than whether any trait is implemented.
This comment has been minimized.
This comment has been minimized.
Ericson2314
Jul 3, 2015
Contributor
On the other hand, seeing that mem::forget is safe, I don't know why we need to care about circumnavigating user-defined destructors at all.
This comment has been minimized.
This comment has been minimized.
aidancully
Jul 3, 2015
Author
Well, just because mem::forget is safe, doesn't mean we should be encouraging its use...
This comment has been minimized.
This comment has been minimized.
|
The point of manual drops it to override the default behavior (duh). So the final drop behavior and the default exist in a sort of self-super relationship. The point of IMO it is simpler/less magical just to expose a different function which is the default The vast majority of implementations will not need to worry about infinite recursion, as all interesting things to do in a |
This comment has been minimized.
This comment has been minimized.
I think that would be acceptable if infinite recursion were the only concern... But it doesn't solve the problem of allowing partial moves from the value during the drop-hook: partial moves are still disallowed for types implementing I know there's been discussion of using destructuring to allow consumption of the container type without consuming the fields, which would inhibit invocation of the drop-hook while continuing to provide access to fields. The difficulty I've had with this approach is that I haven't seen how it would work with some kinds of enums, which don't have moving destructures: enum Foo {
Bar,
// the way this enum gets destructured might depend on its discriminant:
//Baz(Box<u32>),
// but in this case, doesn't really...
Qux,
}
impl DropValue for Foo {
fn drop_value(self) {
// use `match` to find the discriminant:
match self {
when Bar => (),
when Qux => (),
}
// "self" is still accessible? argh, must use `forget`.
mem::forget(self)
}
}When this came up before, I suggested a |
This comment has been minimized.
This comment has been minimized.
|
@aidancully Thanks, I hadn't realized the problems with Copy before. Exposing a "default method" thing would help with the boilerplate, but not with Copy before. [Nor am I willing just say a type can't be DropValue and Copy, linear types that impl Copy might perhaps be useful].
|
This comment has been minimized.
This comment has been minimized.
|
@Ericson2314 You currently cannot impl |
eddyb
reviewed
Jul 4, 2015
| } | ||
| ``` | ||
|
|
||
| Define the following bare functions to convert between `T` and |
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
aidancully
Jul 4, 2015
Author
Because I wanted to avoid possibility of namespace collision. This is nonsensical, but demonstrates the problem I was trying to avoid:
struct Foo;
impl Foo {
fn of(&self) -> &Foo {
self
}
}If we have impl Deref for Interior<T> with Target == T, then there's an ambiguity in how of would be resolved, right?
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
aidancully
Jul 4, 2015
Author
OK, right - we can make these all static methods on Interior. I played it a little too conservative.
This comment has been minimized.
This comment has been minimized.
|
@eddyb OK, but then we don't even need |
This comment has been minimized.
This comment has been minimized.
|
@Ericson2314 the problem isn't with a type that implements Copy and Drop; the problem is with a type that implements Drop, but all the members are Copy. There's also the issue that making an unmarked pattern match skip dropping a value might be a bit too easy to screw up. |
This comment has been minimized.
This comment has been minimized.
|
Ah, sorry I was confused. |
This comment has been minimized.
This comment has been minimized.
|
One of the reasons I haven't written up let x = 0u32;
let y = move x; // x is no longer accessible
fn blah(arg: u32) {}
blah(move y);
#[derive(Copy,Clone)]
struct Foo(u32, u32);
let z = Foo(0, 1);
let (a0, a1) = move z;Of course this likely conflicts with other uses of |
This comment has been minimized.
This comment has been minimized.
|
@aidancully I really don't see the utility of |
This comment has been minimized.
This comment has been minimized.
|
It seems to me that the move should actually be part of the pattern, as we are trying to overrides and "optimization" where patterns that would otherwise consume something don't. Also this allows |
This comment has been minimized.
This comment has been minimized.
|
Given that Infinite recursion can be avoided by either:
|
This comment has been minimized.
This comment has been minimized.
|
@Diggsey ... "sufficiently rare occurrence"? |
This comment has been minimized.
This comment has been minimized.
|
@eddyb It's rare if you continue to use old It seems like a fairly straightforward choice - either you special-case |
This comment has been minimized.
This comment has been minimized.
|
@Diggsey I thought the whole point of this proposal was to allow moving out of I should also mention that current |
This comment has been minimized.
This comment has been minimized.
|
What exactly is a partial move? I assume that if you move a non-copy value out of a non-copy value, it consumes the original. I agree with @Diggsey that the only Drop implementation that shouldn't easily consume
Certainly for backwards compatibility |
This comment has been minimized.
This comment has been minimized.
|
@Ericson2314 if you want to move out of |
This comment has been minimized.
This comment has been minimized.
|
@eddyb Sorry for the confusion, I am talking about the alternative of having The privacy-based rule would only allow one to "peel off" the destructor through destructuring when all fields are visible. |
This comment has been minimized.
This comment has been minimized.
OK, let me present an alternative which has
|
This comment has been minimized.
This comment has been minimized.
|
@Diggsey uh oh, all of those are pretty much hacks: I really can't see how that could be accepted without proper refinement types describing the partially-moved-out state, and those are even further out than |
This comment has been minimized.
This comment has been minimized.
|
I think that doesn't work, since |
aidancully
referenced this pull request
Aug 24, 2015
Closed
should struct fields and array elements be dropped in reverse declaration order (a la C++) #744
This comment has been minimized.
This comment has been minimized.
|
@pnkfelix this RFC hasn't had any comments in a year, should we FCP? Could you summarise the status please? |
This comment has been minimized.
This comment has been minimized.
|
@nrc if we got |
This comment has been minimized.
This comment has been minimized.
|
Given that #1444 was accepted, this isn't very important. SmallVec can be implemented in a straightforward manner on top of a union. And it's possible to write a wrapper type which allows zero-overhead safe consume-on-drop:
|
This comment has been minimized.
This comment has been minimized.
|
@eefriedman I think moving-drop is still valuable because a) it accurately reflects the semantics of destruction and b) it confines all mandatory unsafety to |
This comment has been minimized.
This comment has been minimized.
|
Hear ye, hear ye! This RFC is now entering final comment period. The @rust-lang/lang team is inclined to close, since the addition of unsafe unions allows for things like the small vector use case to be handled that way, and in general there doesn't seem to be a great amount of urgency for this change. |
nikomatsakis
added
the
final-comment-period
label
Aug 12, 2016
This comment has been minimized.
This comment has been minimized.
|
Could we close with the understanding that it could be revisited if we get |
This comment has been minimized.
This comment has been minimized.
For some definitions of "safe". I still support something like this RFC. Looking at current alternatives: I find neither the Option version nor the union version terribly ergonomic, in particular because they require |
This comment has been minimized.
This comment has been minimized.
|
My main motivation is, well, a belief that |
This comment has been minimized.
This comment has been minimized.
|
@Ericson2314 implementing drop glue by yourself sucks :( |
This comment has been minimized.
This comment has been minimized.
|
@ubsan hmm? I may have personally mused on a no-auto-drop-glue world :), but this RFC doesn't make it manual, especially that in the presence of partial moves. |
This comment has been minimized.
This comment has been minimized.
|
Is this a safe public interface? pub union NoDrop<T> {
inner: T,
}
impl<T> NoDrop<T> {
fn into_inner(self) -> T { /*...*/ }
}
impl<T> From<T> for NoDrop<T> { /*...*/ }
impl<T> Deref for NoDrop<T> {
type Target = T;
//...
}
impl<T> DerefMut for NoDrop<T> { /*...*/ }I think so, and I think it would fix the ergonomics issues I was talking about before. |
This comment has been minimized.
This comment has been minimized.
|
@Ericson2314 Afaict, any person that implements |
This comment has been minimized.
This comment has been minimized.
|
@ubsan hmm |
This comment has been minimized.
This comment has been minimized.
|
@Ericson2314 I guess I see it. Still seems... overcomplicated. |
This comment has been minimized.
This comment has been minimized.
|
I'm out of practice with Rust, but I'm not sure why @eefriedman's union is used? Is it just to prevent the interior type from implementing I think the trait DropConsumer: !DropGlue {
fn drop_consume(self);
}
struct ConsumeOnDrop<T: DropConsumer> {
t: T,
}
impl<T: DropConsumer> ConsumeOnDrop<T> {
fn new(t: T) -> Self {
ConsumeOnDrop { t: t }
}
}
impl<T: DropConsumer> Drop for ConsumeOnDrop<T> {
fn drop(&mut self) {
unsafe {
ptr::read(&self.t).drop_consume()
}
}
}I don't see this as that different than the RFC... There are still two types, but instead of being Either way, I won't have time in the foreseeable future to put serious effort into this or other RFCs, so I can't object to closing it. |
This comment has been minimized.
This comment has been minimized.
|
@aidancully union types don't have any drop glue. union Forget<T> { t: T }
Forget { t: ... };is equivalent to std::mem::forget(...); |
This comment has been minimized.
This comment has been minimized.
|
@ubsan, Thanks for pointing that out, I didn't read the union RFC carefully enough and missed that. Using I think my point that consuming drop, while possible, would never become as ergonomic as |
This comment has been minimized.
This comment has been minimized.
I'm aware you feel this way, but I continue to think this is an exaggeration. From the point of view of the Rust type system guarantees alone, Certainly it's true that Drop marks a state transition, and if you consider an extended type, the extended permissions on drop are thus different from other methods that use None of this is to say that I think |
This comment has been minimized.
This comment has been minimized.
|
In that case I think that |
This comment has been minimized.
This comment has been minimized.
|
Agree we should close - we might want to consider something like this, but I think we should re-evaluate with unions, and after considering |
This comment has been minimized.
This comment has been minimized.
|
@Diggsey I think it's exactly equivalent. class This {
...
public:
...
~This() {
...
}
// not
~This() && {
...
}
}; |
This comment has been minimized.
This comment has been minimized.
|
Thanks everyone for the discussion. The @rust-lang/lang team has decided to close this RFC, as previously proposed. As @Ericson2314 noted, this proposal could be revisited in the future when we have more experience with |
aidancully commentedJun 29, 2015
rendered