I'm posting this hear for preliminary feedback in case there's some obvious reason why this is impossible that I'm not seeing.
Summary
Currently, traits that have an associated type are not object-safe. This RFC proposes to relax this rule for traits whose associated types have a trait bound that is itself object safe.
Example
trait MyTrait {
type AssType: Bound;
}
Under the new rule, the above trait could be made into a trait object where
<dyn MyTrait>::AssType == dyn Bound
assuming Bound
is itself object-safe.
For now, we also require that the associated type is only used un-nested as a method return type in the trait definition, and that it has exactly one non-marker trait bound. Some of these restrictions could be lifted in the future (see future extensions).
Motivation
On their blog, @withoutboats describes a plan to make traits with async methods object-safe.
Since every
async fn
returns a different future type, there’s only one way to dynamically dispatch anasync fn
: dynamically dispatch the future type! That is, the returned future from anyasync
method called on a trait object would beBox<dyn Future>
. The compiler, when generating the vtables for this trait, would generate the necessary shim as well to heap allocate the returned future.
I have two objections to this. Firstly, automatically Box
-ing the result type
gives Box
(which is ostensibly a library type) a very special place in the
language. It also implicitly allocates, something Rust has always managaged to
avoid in the past.
Secondly and more importantly, this is a specific application of a more general
and more generally-useful rule which could apply to associated types other than
the return types of async
methods.
Rust should instead pursue the more general rule. The downside of this is that
it will require implementing ?Sized
return types before we can allow traits with
async
methods to be object-safe.
Guide-level explanation
Suppose we have a trait with an associated type used as a return type.
trait MyTrait {
type AssType: Bound;
fn foo(&self) -> Self::AssType;
}
We can use this trait as a trait object as such:
let t: Box<dyn MyTrait> = ... ;
let a: Box<dyn Bound> = box t.foo();
Similarly, suppose we have a trait with an async method:
trait MyAsyncTrait {
async fn bar();
}
We can call this method on a dyn MyAsyncTrait
and explicitly box the result
to obtain a boxed future.
let t: Box<dyn MyAsyncTrait> = ... ;
let f: Box<dyn Future<Item = ()>> = box t.bar();
Reference-level explanation
The compiler, when generating the vtable for this trait, would also generate the necessary shims to unsize the associated type return values into trait objects.
Future extensions
There are plans to eventually support trait objects of multiple traits, eg.
dyn Debug + Display
. Once this is supported we can remove the limitation that
requires the associated type to have exactly one non-marker trait bound.
Rather than requiring the associated type to only appear un-nested in return position, we could also allow some other forms. For example:
trait MyTrait {
type AssType: Bound;
fn foo() -> Box<Self::AssType>;
}
This could be supported since we allow the conversion Box<T: Bound> -> Box<dyn Bound>
.
More generally, there other kinds of "unsizing" that we could support but don't currently. eg.
Result<T: Bound, E> -> Result<dyn Bound, E>
.Foo<T: Bound> -> Foo<dyn Bound>
wherestruct Foo<T> { ..., last: T }