-
Notifications
You must be signed in to change notification settings - Fork 1.6k
Adjust default object bounds [breaking change] #1156
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
Merged
aturon
merged 3 commits into
rust-lang:master
from
nikomatsakis:adjust-default-object-bounds
Jun 19, 2015
Merged
Changes from all commits
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
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 hidden or 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,243 @@ | ||
- Feature Name: N/A | ||
- Start Date: 2015-06-4 | ||
- RFC PR: (leave this empty) | ||
- Rust Issue: (leave this empty) | ||
|
||
# Summary | ||
|
||
Adjust the object default bound algorithm for cases like `&'x | ||
Box<Trait>` and `&'x Arc<Trait>`. The existing algorithm would default | ||
to `&'x Box<Trait+'x>`. The proposed change is to default to `&'x | ||
Box<Trait+'static>`. | ||
|
||
Note: This is a **BREAKING CHANGE**. The change has | ||
[been implemented][branch] and its impact has been evaluated. It was | ||
[found][crater] to cause **no root regressions** on `crates.io`. | ||
Nonetheless, to minimize impact, this RFC proposes phasing in the | ||
change as follows: | ||
|
||
- In Rust 1.2, a warning will be issued for code which will break when the | ||
defaults are changed. This warning can be disabled by using explicit | ||
bounds. The warning will only be issued when explicit bounds would be required | ||
in the future anyway. | ||
- In Rust 1.3, the change will be made permanent. Any code that has | ||
not been updated by that time will break. | ||
|
||
# Motivation | ||
|
||
When we instituted default object bounds, [RFC 599] specified that | ||
`&'x Box<Trait>` (and `&'x mut Box<Trait>`) should expand to `&'x | ||
Box<Trait+'x>` (and `&'x mut Box<Trait+'x>`). This is in contrast to a | ||
`Box` type that appears outside of a reference (e.g., `Box<Trait>`), | ||
which defaults to using `'static` (`Box<Trait+'static>`). This | ||
decision was made because it meant that a function written like so | ||
would accept the broadest set of possible objects: | ||
|
||
```rust | ||
fn foo(x: &Box<Trait>) { | ||
} | ||
``` | ||
|
||
In particular, under the current defaults, `foo` can be supplied an | ||
object which references borrowed data. Given that `foo` is taking the | ||
argument by reference, it seemed like a good rule. Experience has | ||
shown otherwise (see below for some of the problems encountered). | ||
|
||
This RFC proposes changing the default object bound rules so that the | ||
default is drawn from the innermost type that encloses the trait | ||
object. If there is no such type, the default is `'static`. The type | ||
is a reference (e.g., `&'r Trait`), then the default is the lifetime | ||
`'r` of that reference. Otherwise, the type must in practice be some | ||
user-declared type, and the default is derived from the declaration: | ||
if the type declares a lifetime bound, then this lifetime bound is | ||
used, otherwise `'static` is used. This means that (e.g.) `&'r | ||
Box<Trait>` would default to `&'r Box<Trait+'static>`, and `&'r | ||
Ref<'q, Trait>` (from `RefCell`) would default to `&'r Ref<'q, | ||
Trait+'q>`. | ||
|
||
### Problems with the current default. | ||
|
||
**Same types, different expansions.** One problem is fairly | ||
predictable: the current default means that identical types differ in | ||
their interpretation based on where they appear. This is something we | ||
have striven to avoid in general. So, as an example, this code | ||
[will not type-check](http://is.gd/Yaak1l): | ||
|
||
```rust | ||
trait Trait { } | ||
|
||
struct Foo { | ||
field: Box<Trait> | ||
} | ||
|
||
fn do_something(f: &mut Foo, x: &mut Box<Trait>) { | ||
mem::swap(&mut f.field, &mut *x); | ||
} | ||
``` | ||
|
||
Even though `x` is a reference to a `Box<Trait>` and the type of | ||
`field` is a `Box<Trait>`, the expansions differ. `x` expands to `&'x | ||
mut Box<Trait+'x>` and the field expands to `Box<Trait+'static>`. In | ||
general, we have tried to ensure that if the type is *typed precisely | ||
the same* in a type definition and a fn definition, then those two | ||
types are equal (note that fn definitions allow you to omit things | ||
that cannot be omitted in types, so some types that you can enter in a | ||
fn definition, like `&i32`, cannot appear in a type definition). | ||
|
||
Now, the same is of course true for the type `Trait` itself, which | ||
appears identically in different contexts and is expanded in different | ||
ways. This is not a problem here because the type `Trait` is unsized, | ||
which means that it cannot be swapped or moved, and hence the main | ||
sources of type mismatches are avoided. | ||
|
||
**Mental model.** In general the mental model of the newer rules seems | ||
simpler: once you move a trait object into the heap (via `Box`, or | ||
`Arc`), you must explicitly indicate whether it can contain borrowed | ||
data or not. So long as you manipulate by reference, you don't have | ||
to. In contrast, the current rules are more subtle, since objects in | ||
the heap may still accept borrowed data, if you have a reference to | ||
the box. | ||
|
||
**Poor interaction with the dropck rules.** When implementing the | ||
newer dropck rules specified by [RFC 769], we found a | ||
[rather subtle problem] that would arise with the current defaults. | ||
The precise problem is spelled out in appendix below, but the TL;DR is | ||
that if you wish to pass an array of boxed objects, the current | ||
defaults can be actively harmful, and hence force you to specify | ||
explicit lifetimes, whereas the newer defaults do something | ||
reasonable. | ||
|
||
# Detailed design | ||
|
||
The rules for user-defined types from RFC 599 are altered as follows | ||
(text that is not changed is italicized): | ||
|
||
- *If `SomeType` contains a single where-clause like `T:'a`, where | ||
`T` is some type parameter on `SomeType` and `'a` is some | ||
lifetime, then the type provided as value of `T` will have a | ||
default object bound of `'a`. An example of this is | ||
`std::cell::Ref`: a usage like `Ref<'x, X>` would change the | ||
default for object types appearing in `X` to be `'a`.* | ||
- If `SomeType` contains no where-clauses of the form `T:'a`, then | ||
the "base default" is used. The base default depends on the overall context: | ||
- in a fn body, the base default is a fresh inference variable. | ||
- outside of a fn body, such in a fn signature, the base default | ||
is `'static`. | ||
Hence `Box<X>` would typically be a default of `'static` for `X`, | ||
regardless of whether it appears underneath an `&` or not. | ||
(Note that in a fn body, the inference is strong enough to adopt `'static` | ||
if that is the necessary bound, or a looser bound if that would be helpful.) | ||
- *If `SomeType` contains multiple where-clauses of the form `T:'a`, | ||
then the default is cleared and explicit lifetiem bounds are | ||
required. There are no known examples of this in the standard | ||
library as this situation arises rarely in practice.* | ||
|
||
# Timing and breaking change implications | ||
|
||
This is a breaking change, and hence it behooves us to evaluate the | ||
impact and describe a procedure for making the change as painless as | ||
possible. One nice propery of this change is that it only affects | ||
*defaults*, which means that it is always possible to write code that | ||
compiles both before and after the change by avoiding defaults in | ||
those cases where the new and old compiler disagree. | ||
|
||
The estimated impact of this change is very low, for two reasons: | ||
- A recent test of crates.io found [no regressions][crater] caused by | ||
this change (however, a [previous run] (from before Rust 1.0) found 8 | ||
regressions). | ||
- This feature was only recently stabilized as part of Rust 1.0 (and | ||
was only added towards the end of the release cycle), so there | ||
hasn't been time for a large body of dependent code to arise | ||
outside of crates.io. | ||
|
||
Nonetheless, to minimize impact, this RFC proposes phasing in the | ||
change as follows: | ||
|
||
- In Rust 1.2, a warning will be issued for code which will break when the | ||
defaults are changed. This warning can be disabled by using explicit | ||
bounds. The warning will only be issued when explicit bounds would be required | ||
in the future anyway. | ||
- Specifically, types that were written `&Box<Trait>` where the | ||
(boxed) trait object may contain references should now be written | ||
`&Box<Trait+'a>` to disable the warning. | ||
- In Rust 1.3, the change will be made permanent. Any code that has | ||
not been updated by that time will break. | ||
|
||
# Drawbacks | ||
|
||
The primary drawback is that this is a breaking change, as discussed | ||
in the previous section. | ||
|
||
# Alternatives | ||
|
||
Keep the current design, with its known drawbacks. | ||
|
||
# Unresolved questions | ||
|
||
None. | ||
|
||
# Appendix: Details of the dropck problem | ||
|
||
This appendix goes into detail about the sticky interaction with | ||
dropck that was uncovered. The problem arises if you have a function | ||
that wishes to take a mutable slice of objects, like so: | ||
|
||
```rust | ||
fn do_it(x: &mut [Box<FnMut()>]) { ... } | ||
``` | ||
|
||
Here, `&mut [..]` is used because the objects are `FnMut` objects, and | ||
hence require `&mut self` to call. This function in turn is expanded | ||
to: | ||
|
||
```rust | ||
fn do_it<'x>(x: &'x mut [Box<FnMut()+'x>]) { ... } | ||
``` | ||
|
||
Now callers might try to invoke the function as so: | ||
|
||
```rust | ||
do_it(&mut [Box::new(val1), Box::new(val2)]) | ||
``` | ||
|
||
Unfortunately, this code fails to compile -- in fact, it cannot be | ||
made to compile without changing the definition of `do_it`, due to a | ||
sticky interaction between dropck and variance. The problem is that | ||
dropck requires that all data in the box strictly outlives the | ||
lifetime of the box's owner. This is to prevent cyclic | ||
content. Therefore, the type of the objects must be `Box<FnMut()+'R>` | ||
where `'R` is some region that strictly outlives the array itself (as | ||
the array is the owner of the objects). However, the signature of | ||
`do_it` demands that the reference to the array has the same lifetime | ||
as the trait objects within (and because this is an `&mut` reference | ||
and hence invariant, no approximation is permitted). This implies that | ||
the array must live for at least the region `'R`. But we defined the | ||
region `'R` to be some region that outlives the array, so we have a | ||
quandry. | ||
|
||
The solution is to change the definition of `do_it` in one of two | ||
ways: | ||
|
||
```rust | ||
// Use explicit lifetimes to make it clear that the reference is not | ||
// required to have the same lifetime as the objects themselves: | ||
fn do_it1<'a,'b>(x: &'a mut [Box<FnMut()+'b>]) { ... } | ||
|
||
// Specifying 'static is easier, but then the closures cannot | ||
// capture the stack: | ||
fn do_it2(x: &'a mut [Box<FnMut()+'static>]) { ... } | ||
``` | ||
|
||
Under the proposed RFC, `do_it2` would be the default. If one wanted | ||
to use lifetimes, then one would have to use explicit lifetime | ||
overrides as shown in `do_it1`. This is consistent with the mental | ||
model of "once you box up an object, you must add annotations for it | ||
to contain borrowed data". | ||
|
||
[RFC 599]: 0599-default-object-bound.md | ||
[RFC 769]: 0769-sound-generic-drop.md | ||
[rather subtle problem]: https://github.com/rust-lang/rust/pull/25212#issuecomment-100244929 | ||
[crater]: https://gist.github.com/brson/085d84d43c6a9a8d4dc3 | ||
[branch]: https://github.com/nikomatsakis/rust/tree/better-object-defaults | ||
[previous run]: https://gist.github.com/brson/80f9b80acef2e7ab37ee | ||
[RFC 1122]: https://github.com/rust-lang/rfcs/pull/1122 |
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.
nit: "propery" --> "property"