7.7 KiB
- Start Date: 2014-09-22
- RFC PR: rust-lang/rfcs#255
- Rust Issue: rust-lang/rust#17670
Summary
Restrict which traits can be used to make trait objects.
Currently, we allow any traits to be used for trait objects, but restrict the methods which can be called on such objects. Here, we propose instead restricting which traits can be used to make objects. Despite being less flexible, this will make for better error messages, less surprising software evolution, and (hopefully) better design. The motivation for the proposed change is stronger due to part of the DST changes.
Motivation
Part of the planned, in progress DST work is to allow trait objects where a trait is expected. Example:
fn foo<Sized? T: SomeTrait>(y: &T) { ... }
fn bar(x: &SomeTrait) {
foo(x)
}
Previous to DST the call to foo
was not expected to work because SomeTrait
was not a type, so it could not instantiate T
. With DST this is possible, and
it makes intuitive sense for this to work (an alternative is to require impl SomeTrait for SomeTrait { ... }
, but that seems weird and confusing and rather
like boilerplate. Note that the precise mechanism here is out of scope for this
RFC).
This is only sound if the trait is object-safe. We say a method m
on trait
T
is object-safe if it is legal (in current Rust) to call x.m(...)
where x
has type &T
, i.e., x
is a trait object. If all methods in T
are object-safe,
then we say T
is object-safe.
If we ignore this restriction we could allow code such as the following:
trait SomeTrait {
fn foo(&self, other: &Self) { ... } // assume self and other have the same concrete type
}
fn bar<Sized? T: SomeTrait>(x: &T, y: &T) {
x.foo(y); // x and y may have different concrete types, pre-DST we could
// assume that x and y had the same concrete types.
}
fn baz(x: &SomeTrait, y: &SomeTrait) {
bar(x, y) // x and y may have different concrete types
}
This RFC proposes enforcing object-safety when trait objects are created, rather than where methods on a trait object are called or where we attempt to match traits. This makes both method call and using trait objects with generic code simpler. The downside is that it makes Rust less flexible, since not all traits can be used to create trait objects.
Software evolution is improved with this proposal: imagine adding a non-object-safe method to a previously object-safe trait. With this proposal, you would then get errors wherever a trait-object is created. The error would explain why the trait object could not be created and point out exactly which method was to blame and why. Without this proposal, the only errors you would get would be where a trait object is used with a generic call and would be something like "type error: SomeTrait does not implement SomeTrait" - no indication that the non-object-safe method were to blame, only a failure in trait matching.
Another advantage of this proposal is that it implies that all method-calls can always be rewritten into an equivalent UFCS call. This simplifies the "core language" and makes method dispatch notation -- which involves some non-trivial inference -- into a kind of "sugar" for the more explicit UFCS notation.
Detailed design
To be precise about object-safety, an object-safe method must meet one of the following conditions:
- require
Self : Sized
; or, - meet all of the following conditions:
- must not have any type parameters; and,
- must have a receiver that has type
Self
or which dereferences to theSelf
type;- for now, this means
self
,&self
,&mut self
, orself: Box<Self>
, but eventually this should be extended to custom types likeself: Rc<Self>
and so forth.
- for now, this means
- must not use
Self
(in the future, where we allow arbitrary types for the receiver,Self
may only be used for the type of the receiver and only where we allowSized?
types).
A trait is object-safe if all of the following conditions hold:
- all of its methods are object-safe; and,
- the trait does not require that
Self : Sized
(see also RFC 546).
When an expression with pointer-to-concrete type is coerced to a trait object, the compiler will check that the trait is object-safe (in addition to the usual check that the concrete type implements the trait). It is an error for the trait to be non-object-safe.
Note that a trait can be object-safe even if some of its methods use
features that are not supported with an object receiver. This is true
when code that attempted to use those features would only work if the
Self
type is Sized
. This is why all methods that require
Self:Sized
are exempt from the typical rules. This is also why
by-value self methods are permitted, since currently one cannot invoke
pass an unsized type by-value (though we consider that a useful future
extension).
Drawbacks
This is a breaking change and forbids some safe code which is legal
today. This can be addressed in two ways: splitting traits, or adding
where Self:Sized
clauses to methods that cannot not be used with
objects.
Example problem
Here is an example trait that is not object safe:
trait SomeTrait {
fn foo(&self) -> int { ... }
// Object-safe methods may not return `Self`:
fn new() -> Self;
}
Splitting a trait
One option is to split a trait into object-safe and non-object-safe parts. We hope that this will lead to better design. We are not sure how much code this will affect, it would be good to have data about this.
trait SomeTrait {
fn foo(&self) -> int { ... }
}
trait SomeTraitCtor : SomeTrait {
fn new() -> Self;
}
Adding a where-clause
Sometimes adding a second trait feels like overkill. In that case, it
is often an option to simply add a where Self:Sized
clause to the
methods of the trait that would otherwise violate the object safety
rule.
trait SomeTrait {
fn foo(&self) -> int { ... }
fn new() -> Self
where Self : Sized; // this condition is new
}
The reason that this makes sense is that if one were writing a generic
function with a type parameter T
that may range over the trait
object, that type parameter would have to be declared ?Sized
, and
hence would not have access to the new
method:
fn baz<T:?Sized+SomeTrait>(t: &T) {
let v: T = SomeTrait::new(); // illegal because `T : Sized` is not known to hold
}
However, if one writes a function with sized type parameter, which
could never be a trait object, then the new
function becomes
available.
fn baz<T:SomeTrait>(t: &T) {
let v: T = SomeTrait::new(); // OK
}
Alternatives
We could continue to check methods rather than traits are object-safe. When checking the bounds of a type parameter for a function call where the function is called with a trait object, we would check that all methods are object-safe as part of the check that the actual type parameter satisfies the formal bounds. We could probably give a different error message if the bounds are met, but the trait is not object-safe.
We might in the future use finer-grained reasoning to permit more
non-object-safe methods from appearing in the trait. For example, we
might permit fn foo() -> Self
because it (implicitly) requires that
Self
be sized. Similarly, we might permit other tests beyond just
sized-ness. Any such extension would be backwards compatible.
Unresolved questions
N/A
Edits
- 2014-02-09. Edited by Nicholas Matsakis to (1) include the
requirement that object-safe traits do not require
Self:Sized
and (2) specify that methods may includewhere Self:Sized
to overcome object safety restrictions.