RFC 0255: object-safety

lang (traits | typesystem | trait-object | machine | coercions)

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:

A trait is object-safe if all of the following conditions hold:

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