(For some additional background, see the “Pre-pre-RFC: Anti-trait-object auto trait”. cc @mikeyhew)
- Feature Name: anti_trait_object
- Start Date: 2018-12-21
- RFC PR:
- Rust Issue:
Summary
Add a new auto trait to the core
library that can be used in a trait’s methods’ where
clauses to make the trait object safe.
Motivation
Object safety establishes some requirements that traits must meet before they can be used as a trait object (see RFC 255 and Huon’s blog post for more details on object safety). Some method types (like static or generic methods) make a trait not object safe. The only escape hatch at present is adding a where Self: Sized
clause, like so:
trait Trait {
fn foo() where Self: Sized;
fn bar<T>() where Self: Sized;
}
Here, the added where Self: Sized
clauses are required to make Trait
object safe. This works because the types dyn Trait
, dyn Trait + ...
, and dyn SubTrait
(where SubTrait
is trait SubTrait: Trait { ... }
) are not Sized
, and thus do not have the problematic methods foo
and bar
despite implementing Trait
.
But where Self: Sized
is a blunt hammer that is overly broad. It prevents (non-trait-object) unsized types (like str
, [T]
, and extern types) from implementing these methods. This RFC aims to address this and provide a more precise where
clause that traits can use to comply with object safety while still allowing other unsized types to fully implement the trait.
Guide-level explanation
The trait std::marker::ExcludesDynType<T>
is implemented for all types except those which include the trait object type T
. That is, given a trait named Trait
, the trait ExcludesDynType<dyn Trait>
is implemented for all types except:
dyn Trait
dyn Trait + ...
-
dyn SubTrait
(whereSubTrait
is a subtrait ofTrait
(i.e.,trait SubTrait: Trait { ... }
)). dyn SubTrait + ...
ExcludesDynType<T>
is only useful if T
is a trait object type. If T
is a non-trait-object type, then ExcludesDynType<T>
is implemented for all types (though you should avoid intentionally passing a non-trait-object type parameter given its uselessness).
The ExcludesDynType<T>
trait is useful in where
clauses to limit trait methods to make the trait object safe (see RFC 255 and Huon Wilson’s blog post on object safety for more information on trait objects and object safety).
For example, the following trait is not object safe:
trait Trait {
fn foo();
fn bar<T>(&self);
}
The methods Trait::foo()
and Trait::bar()
cannot be dynamically dispatched safely on the trait object type dyn Trait
. Historically, this was fixed by using a where Self: Sized
clause, like so:
trait Trait {
fn foo() where Self: Sized;
fn bar<T>(&self) where Self: Sized;
}
This limits the methods in Trait
to being applied to only Sized
types. Since trait objects are unsized, these methods cannot be called on dyn Trait
, and thus Trait
is now object safe. However, this prevents perfectly valid unsized types (like str
, [T]
, and extern types) from implementing these trait methods.
Instead of where Self: Sized
, you should use where Self: ExcludesDynType<dyn Trait>
(where Trait
is the trait name) to limit these problematic methods such that they cannot be called on dyn Trait
, thus satisfying trait object safety requirements without preventing valid unsized types from implementing the trait. We can fix the previous code like so:
trait Trait {
fn foo() where Self: ExcludesDynType<dyn Trait>;
fn bar<T>(&self) where Self: ExcludesDynType<dyn Trait>;
}
This will prevent Trait::foo()
and Trait::bar()
from being called on dyn Trait
, dyn Trait + ...
, dyn SubTrait
, and dyn SubTrait + ...
(where SubTrait
is a subtrait of Trait
(i.e., trait SubTrait: Trait { ... }
)). The trait Trait
now fully satifies all object safety requirements.
Reference-level explanation
The trait ExcludesDynType<T>
is a marker trait, and as such should be placed in std::marker
. It is a lang-level trait.
/// This trait is automatically implemented for every type, *except*
/// when:
///
/// - The type parameter `T` is a trait object type (e.g., `dyn Trait`)
/// AND
/// - The type in question is a trait object that includes the type
/// parameter `T`
///
/// In other words, if `T` is `dyn Trait`, then this trait is implemented
/// for all types except:
///
/// - `dyn Trait`
/// - `dyn Trait + ...`
/// - `dyn SubTrait` (where `SubTrait` is a subtrait of `Trait` (i.e.
/// `trait SubTrait: Trait { ... }`))
/// - `dyn SubTrait + ...` (using the same definition for `SubTrait` as
/// above)
///
/// Like `Sized`, this trait is special and cannot be manually
/// implemented for a type by users.
///
/// This type can be used in a `where` clause for trait methods to meet
/// object safety requirements. For example:
///
/// ```
/// // This trait is object safe because the methods `foo` and `bar`
/// // (which are not object safe) have a where clause that restricts
/// // them from being implemented on types like `dyn Trait`.
/// trait Trait {
/// fn foo() where Self: ExcludesDynType<dyn Trait>;
/// fn bar<T>(&self) where Self: ExcludesDynType<dyn Trait>;
/// }
/// ```
#[lang = "object_safety_trait"]
trait ExcludesDynType<T: ?Sized> {}
Drawbacks
- This requires a new lang-level trait.
- This trait is unlikely to be useful in any other contexts. We would be adding a whole new trait just for these
where
clauses and object safety. - This requires RFC 2027 (“Tweak object safety rules to allow static dispatch”) to be implemented. No one is working on that implementation.
- We’ll need to make sure this doesn’t trip the
where_clauses_object_safety
future compatibility lint.where Self: IsNotDynWithMethodsOf<dyn Trait>
will probably have to be whitelisted within the compiler (likewhere Self: Sized
was).
Rationale and alternatives
Alternative trait names (assuming the trait in question is named Trait
):
IsNotDynWithMethodsOf<dyn Trait>
- Suggestions welcome.
If RFCs 1834 (“Type inequality constraints in where
clauses”) and 2580 (“Pointer metadata & VTable”) are accepted, an alternative where
clause is where <Self as Pointee>::Metadata != &'static VTable
. This where
clause checks whether Self
is not a trait object. Some downsides:
- It’s still more broad than is absolutely necessary. For example, given two totally unrelated traits (named
Foo
andBar
),dyn Foo
can implementBar
, but thiswhere
clause would preventdyn Foo
from implementingBar
's non-object-safe methods. - It’s unintuitive. It’s not clear what the
where
clause is really checking for and requires familiarity with trait objects, object safety, thePointee
trait, and how trait object pointers are represented (particularly with regard to thePointee
trait).
That said, this alternative where
clause could be supported in addition to the new trait proposed in this RFC. This alternative where
clause is a natural consequence of RFCs 1834 and 2580.
We could also drop the generic type parameter from the proposed trait, rename it to something like IsNotTraitObject
, and then implement it for all types that are not trait objects. This is effectively the same as the above alternative, except that it does not rely on RFCs 1834 and 2580. It shares the downside that it is more broad than is absolutely necessary.
With RFC 2027 (“Tweak object safety rules to allow static dispatch”), it might appear this RFC proposal is unnecessary and that a non-object-safe trait named Trait
can just use where Self: Trait
since dyn Trait
will not implement Trait
if it is not object safe. But this doesn’t work since where Self: Trait
can make the trait object safe, thus making dyn Trait
implement Trait
, thus satisfying the where
clause, thus making the trait no longer object safe, etc. There might be ways to work around this and successfully apply this tautological type of where
clause, but this RFC does not explore this further (and any potential ramifications beyond trait objects and object safety).
The Trait-parametric Polymorphism RFC may introduce new syntax and features that could warrant changes to this RFC.
Prior art
I am not familiar with any prior art. See the prior discussion on where Self: Sized
for the closest thing to prior art that I could find.
Unresolved questions
- The name of the trait.
Future possibilities
C++ has a type_traits
header for assisting template metaprogramming and specialization. Rust is working on specialization and could potentially benefit from a suite of traits analogous to C++'s type_traits
. The new trait proposed in this RFC could be part of this and should be designed with that end in mind.