Pre-RFC: A new auto trait for precise object-safe where-clauses

(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 (where SubTrait is a subtrait of Trait (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

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 and Bar), dyn Foo can implement Bar, but this where clause would prevent dyn Foo from implementing Bar'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, the Pointee trait, and how trait object pointers are represented (particularly with regard to the Pointee 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.

1 Like

A quick read of your alternatives doesn’t make it clear why having just inequality where clauses doesn’t let me write

trait T {
  fn foo() where Self != dyn T;
}

This proposal just looks like restricted where inequality with extra steps.

1 Like

Because there are 4 types that need to be excluded:

  • dyn T
  • dyn T + SomeOtherTraits
  • dyn SubTrait
  • dyn SubTrait + SomeOtherTraits

I originally proposed where Self != dyn T in the discussion thread, but @mikeyhew pointed out the other types that need exclusion.

2 Likes

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.