-
Notifications
You must be signed in to change notification settings - Fork 1.6k
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
Allow Eager Drops #239
Closed
Closed
Allow Eager Drops #239
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,185 @@ | ||
- Start Date: 14-09-2014 | ||
- RFC PR: | ||
- Rust Issue: | ||
|
||
# Summary | ||
|
||
Allow the compiler to drop objects that are no longer used before the end of | ||
their scope. Rust's type system ensures that objects aren't dropped too soon. | ||
The programmer can explicitly specify an object's lifetime if necessary. | ||
Removes the need for drop flags. | ||
|
||
# Motivation | ||
|
||
In C++, it is the programmer's responsibility to ensure an object isn't used | ||
after it is destroyed. To facilitate this, it provides specific rules for when | ||
objects get destroyed (at the end of scope, in reverse order of construction). | ||
This is very important because the compiler doesn't know which objects depend | ||
on which others. | ||
|
||
Rust currently follows this practice of dropping objects at the end of scope. | ||
However, Rust's type system is much more powerful than that of C++, allowing | ||
lifetime dependencies between objects to be specified and enforced at compile | ||
time. The vast majority of the time, it doesn't matter where inside its scope | ||
an object is dropped as long as all lifetime dependencies are met. This even | ||
extends to concurrency types such as `Mutex`, since the protected object must | ||
be accessed through the protecting `Mutex`. | ||
|
||
By allowing types to be dropped as soon as they are no longer used, we would | ||
further encourage programmers to properly utilize the type system to specify | ||
lifetime dependencies instead of implicitly relying on the current drop | ||
semantics. This would result in safer code, since the compiler would be able to | ||
catch accidental violations of the lifetime dependencies that may be introduced | ||
in the future. | ||
|
||
For the rare case that the programmer wants to control an object's lifetime | ||
independently of its dependencies with other objects (e.g., if dropping an | ||
object is time consuming and shouldn't happen in the middle of a | ||
timing-sensitive operation), the programmer would be required to specify the | ||
lifetime explicitly. This makes it clear that the specific lifetime of the | ||
object is important and prevents accidental earlier moves and drops. | ||
|
||
It is currently possible for one code path to move an object an another not to. | ||
Currently, Rust handles this case by adding a flag to each object implementing | ||
`Drop` indicating whether or not it has already been dropped. Because an object | ||
can't be used after it has conditionally been moved, allowing eager drops would | ||
enable the compiler to statically ensure that any objects moved or dropped in | ||
one branch are also moved or dropped in the other, eliminating the need for | ||
drop flags. See [RFC PR 210](https://github.com/rust-lang/rfcs/pull/210) for a | ||
discussion of why the current situation is far from ideal. | ||
|
||
Allowing eager drops could potentially even reduce memory usage. Because the | ||
stack space of objects that are no longer needed could be reused for newly | ||
created objects, the total stack size required by a function may be able to be | ||
reduced. Additionally, any heap memory controlled by an object could also | ||
potentially be freed sooner. | ||
|
||
Finally, there has been some discussion about shortening the duration of | ||
borrows from their current lexical basis in order to allow greater borrowing | ||
flexibility. Allowing eager drops would generalize this idea to objects instead | ||
of restricting it to references. Thus, the same flexibility could be obtained | ||
when using objects that hold references. | ||
|
||
# Detailed design | ||
|
||
The compiler may drop an object an any time as long as the following conditions | ||
are met: | ||
- There are no future uses of the object | ||
- The object is not currently borrowed | ||
- There is no other existing object which the object in question must outlive | ||
|
||
Additionally, an object must be dropped when leaving its scope through any | ||
means. | ||
|
||
To explicitly control an object's lifetime, the programmer would call drop at | ||
the point where they want the objects lifetime to end. Since calling drop | ||
counts as a use of the object, the object would be guaranteed to be freed at | ||
exactly that point. Additionally, any attempt to move or partially move from | ||
the object (even conditionally) would be an error, catching any accidental | ||
moves. (In the very rare event that the programmer wants to move or drop an | ||
object in one case and explicitly extend its lifetime in another, they would be | ||
forced to use an `Option`.) | ||
|
||
# Drawbacks | ||
|
||
- Might be surprising to those coming from C++ | ||
|
||
- Existing code (especially unsafe code) must be audited to ensure it is not | ||
implicitly relying on the current drop semantics | ||
|
||
- Harder to tell exactly when an object will be dropped | ||
|
||
This shouldn't be an issue in practice. Most of the time one doesn't care as | ||
long as lifetime dependencies are satisfied, and one can drop explicitly when | ||
one does care. | ||
|
||
- Code specifically examining and manipulating drop flags would have to be | ||
changed | ||
|
||
# Alternatives | ||
|
||
Several alternatives have been suggested to specifically address the current | ||
drop flags: | ||
|
||
- Unbalanced drops are an error | ||
|
||
This would require that all objects that are moved or dropped in one branch | ||
be moved or dropped in every other branch. This was found to be too painful. | ||
|
||
- Only allow the compiler to drop objects before the end of scope in the case | ||
of unbalanced conditional drops. | ||
|
||
This is the gist of [RFC PR 210](https://github.com/rust-lang/rfcs/pull/210). | ||
This approach attempts to be more conservative, changing the current | ||
semantics the minimum amount necessary to avoid drop flags. Unfortunately, | ||
this at the expense of consistency. This has many of the same drawbacks as | ||
more general eager drops, but misses out on many of the advantages outlined | ||
above. It still allows one to implicitly rely on drops occurring at the end | ||
of the scope, while at the same time introducing a corner case by which an | ||
object can be implicitly dropped early, increasing the possibility of a | ||
mistake. The RFC attempts to mitigate this danger through a system of traits | ||
and lints, but this is complex and ties the warnings to specific types, which | ||
is not ideal (see below). | ||
|
||
- Stack based dynamic drop flags | ||
|
||
The biggest problem with the current implementation of drop flags is that it | ||
adds a drop flag to every single object implementing `Drop`, regardless of | ||
whether that object is ever conditionally moved or dropped, or even could be. | ||
Instead of doing this, it would be possible to keep drop flags on the stack, | ||
and only store and check them for objects that are actually conditionally | ||
moved. This would allow us to maintain the current semantics while | ||
substantially reducing the overhead. | ||
|
||
Additionally, there has been some discussion about determining when to drop an | ||
object based on its type, with objects of some types having eager drop | ||
semantics and objects of other types having scope-based drop semantics. | ||
Unfortunately, this doesn't really work because the types about whose lifetimes | ||
the programmer cares is highly dependent on context. Furthermore, it would make | ||
object lifetimes inconsistent and more complex. | ||
|
||
As an example, if one is programming a timing-sensitive, real-time routine, | ||
taking the time to free a large tree in the middle may be completely | ||
unacceptable. On the other hand, if one is doing a long calculation, performing | ||
a drop that performs a large write to disk part way through the calculation | ||
instead of at the end may be perfectly acceptable. | ||
|
||
Because of this, it makes sense for drop semantics of an object to be | ||
controlled by the user of the type, not the creator. In the future, if it turns | ||
out there are some types that legitimately do not make sense to use without an | ||
explicit lifetime, we could add a trait or attribute that triggers a warning if | ||
the type is used without one. | ||
|
||
# Unresolved questions | ||
|
||
- Should objects to always be dropped as early as possible? | ||
|
||
Currently, this RFC leaves it up to the compiler to determine the optimal | ||
time to drop an object within the relevant constraints. Would it be useful to | ||
explicitly specify that objects are dropped as soon as they are no longer | ||
used? Are there any optimizations this would hinder? | ||
|
||
- Should this cause the compiler to accept code that wouldn't work today? | ||
|
||
The programmer could potentially take advantage of early drops by, e.g., | ||
moving an object that was being by an object eligible for an eager drop (this | ||
would force the compiler to perform the drop before the move). Is this | ||
desirable? | ||
|
||
- Should there be a `scoped` keyword, attribute, or similar mechanism? | ||
|
||
This RFC avoids adding any additional syntax, using the existing drop | ||
function to explicitly specify an objects lifetime when needed. Some have | ||
suggested that it would be nice to add a `scoped` keyword that can be used in | ||
a `let` statement to specify that a given variable should be dropped at the | ||
end of its scope. This would have have roughly the same effect as calling | ||
`drop` at the end of the scope, including disallowing full or partial | ||
conditional moves. Would this be helpful? | ||
|
||
- How would this interact with fail? | ||
|
||
Under this proposal, it's easy for the compiler to statically determine what | ||
needs to be dropped during normal control flow (e.g., it can determine | ||
exactly what objects are alive when a give return statement is reached). | ||
Would the same be true in the case of stack unwinding, or would task failure | ||
complicate the implementation of this proposal? |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could you give a concrete example of this? The ony way one object could have a lifetime dependency on another object would be if it either owns that object, in which case there can be no problem, or it has a reference, in which case the current borrow checker should catch any problems. Obviously I am misunderstanding something here, and I think an example would help.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I guess I was thinking of something like this, where an object is borrowed by another without actually needing a reference, internally. The ergonomics could probably be improved.