10 KiB
- Feature Name: N/A
- Start Date: 2015-06-04
- RFC PR: rust-lang/rfcs#1156
- Rust Issue: rust-lang/rust#26438
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 and its impact has been evaluated. It was
found 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:
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:
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 likeT:'a
, whereT
is some type parameter onSomeType
and'a
is some lifetime, then the type provided as value ofT
will have a default object bound of'a
. An example of this isstd::cell::Ref
: a usage likeRef<'x, X>
would change the default for object types appearing inX
to be'a
. - If
SomeType
contains no where-clauses of the formT:'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
. HenceBox<X>
would typically be a default of'static
forX
, 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 formT:'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 property 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 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.
- Specifically, types that were written
- 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:
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:
fn do_it<'x>(x: &'x mut [Box<FnMut()+'x>]) { ... }
Now callers might try to invoke the function as so:
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:
// 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".