Edit: This turned out not to make any sense.
I read @huon's posts on object safety yesterday. It took some time to wrap my head around everything (as seems to be the case for many people), but I now agree with all the necessary restrictions given the current implementation. However, it seems to me that, with a bit of a change in the design of trait objects, we could lift one or two of the rules.
The first/main one would be the "References Self
" rule. (Code excerpts from the article.) To recap, if a trait has a method that refers to Self
twice, like so:
trait Foo {
fn method(&self, other: &Self);
}
then it is not object-safe, because if we would implement it as follows
impl<'a> Foo for Foo+'a {
fn method(&self, other: &(Foo+'a))
(self.vtable.method)(self.data, /* what goes here? */)
}
}
we cannot rely on other.data
to have the same type as self.data
. But let us try to redefine the trait object type as it is represented in std::raw
:
pub struct Foo<T: Foo> {
pub data: *mut T,
pub vtable: *mut (),
}
Every trait object now encodes the type of the data it points to in its type. (I don't know a lot about the internals, so if the *mut T
doesn't make sense here, throw in some PhantomData
instead.) Therefore, the implicit implementation would look like this:
impl<'a, T: Foo> Foo for Foo<T>+'a {
fn method(&self, other: &(Foo<T>+'a))
(self.vtable.method)(self.data, other.data)
}
}
We can now safely use other.data
since it has the same type as self.data
as per the function signature -- we shift the burden of proof on the caller. (&x as &Foo).method(&y as &Foo)
would become (&x as &Foo<X>).method(&y as Foo<Y>)
(where X
and Y
are the types of x
and y
, respectively), and the compiler can easily verify if X == Y
.
Now we have introduced another type parameter T
here. That would mean that we'd have to monomorphize the impl
for every possible type that implements Foo
, which is in general unbounded. However, neither the structure of the trait object nor the vtable object change: all possible types for T
generate exactly the same code, so in practice, we only need the type parameter only for the function signature. Unless I'm missing something, this is therefore a possible implementation of trait objects that lifts this restriction; the proposed changes only affect the type-checker and do not touch the actual memory representation.
While the change doesn't look backwards-compatible syntax-wise, I think that in all valid cases, the type parameter can be inferred. I think the "Static method" rule can also be lifted at least partially, but that builds on this system, so I guess someone better verify that this makes sense first.