Trait objects: Blocking entire traits vs blocking members


#1

In Rust, trait objects can be created if they follow some specific rules:

  • It does not require Self: Sized
  • It does not have any methods that reference the Self type*
  • It does not have any methods that contain generic type parameters*
  • It does not have any methods that have no receivers*
  • It does not have any associated constants
  • It does not have any supertraits that use Self as a type parameter
  • (*) - Unless said method has a where Self: Sized restriction

These restrictions make sense, however I don’t understand why they prevent the whole trait from being used as a trait object. Why not make it so those specific members that violate those rules can’t be used in trait objects instead of the whole trait being unable to be made into a trait object?


#2

thats what putting where Self: Sized on those members is for (though associated constants can’t have that right now unfortunately).


#3

Historical note: For a long time before 1.0, trait objects worked that way you’re suggesting, where trait objects automatically supported only the subset of methods with allowable signatures. This was changed so that the bound dyn Trait: Trait could hold – i.e. a trait object for trait Trait is itself a type that impls Trait. That allows you to do things like:

fn foo<T: ?Sized + Trait>(t: &T) {
    t.bar();
}

fn bar(trait_object: &dyn Trait) {
    foo(trait_object) // pass a trait object into the generic function
}

If trait objects automatically forbade calling certain methods, then this couldn’t work, because what if foo() tried to call one of those methods?

[Note: dyn Trait is also known as just Trait (when used as a type) in the old syntax.]

[edit2: fixed signature]


#4

Right, it also doesn’t work for Traits with supertraits that require Self: Sized or supertraits that use Self in their type parameters.

For example:

trait Trait : Clone + PartialEq {
    fn safe(&self);
}

What’s to prevent Rust from allowing the member safe from being used while preventing the Clone and PartialEq methods from being used instead of just blocking the entire trait from being used as a trait object?


#5

I think this decision reasoning is no longer convincing right now. We can easily relax the rule to say dyn Trait: Trait if and only if Trait is object safe and enable dyn Trait as a concrete type that implements all the object safe methods only.

Yes this is what I also want to say. In the days dyn Trait = Trait, having Trait:Trait not hold is weird. But not it is no longer the case for dyn Trait:Trait.


#6

The other reason I recall for changing to the current design was that it was easy to write types that were useless. You could write Box<Clone>, and it would compile, but you couldn’t ever do anything with it. Ok, that’s maybe obvious for Clone, but it’s less obvious for more complicated traits where there’s maybe one method that it turns out you can’t dynamically dispatch, and you only find this out after you’ve already done a lot of grunt work.

Having used both, I prefer the explicitness of the current design. If I want a trait to be dynamically dispatched, I have to design it to be dynamically dispatched. It’s not that hard, and if it compiles, I know it’ll work.


#7

In cases like this, the compiler might check if the entire trait requires Sized or if all members require Sized.

This still exists today, except it locks the whole trait out instead of that one method. So either way, you’re getting rid of that method or changing the code around it.


#8

I don’t understand what you’re trying to say here.

Except with how it works now, you’re told it won’t work up-front instead of thinking it works, writing a bunch of code, and only then discovering a method you need isn’t included and having to refactor or even redesign the program. I literally had this exact thing happen to me under the old design.

I like the way things are now for the same reason I like static typing: I want to be told about errors I’ve made as soon as possible. That said, I didn’t come here to argue about this, just to provide an additional point of justification for the current design.


#9

Examples:

// compiler error, no members can be used on trait object
let clone: Box<Clone> = { /* */ }

trait Trait : PartialEq { }

// compiler error, no members can be used on trait object
let trait1: Box<Trait> = { /* */ }

trait Trait2 : PartialEq {
    fn safe(&self);
}
// `safe` can be used, thus no error
let trait2: Box<Trait2> = { /* */ }

trait Trait3 : Trait2 + Clone {
    fn sized_required() -> i32 where Self: Sized;
}
// `safe` from Trait2 can be used, so no error
let trait3: Box<Trait3> = { /* */ }

#10

I don’t think in your examples any of those use case should be a hard error, it should not be more harmful than define an unused variable or function.

The criteria is simple: if a dyn Trait has a zero sized vtable, and dyn Trait:Trait is not hold, warning.

(So, a Box<dyn Clone + Drop> do make sense, if multiple traits in trait object would be allowed.)

In the point of view as a developer, when I use a trait object, this is not implicit: you have to know what you are doing by writing dyn Trait down somewhere. And when you do so, you will need to know what you want from this thing. So just like I don’t have to write many tuples as otherwise useless structs, if I don’t have to name a new trait, this is a grate benefit on quick prototyping or agile development.

This is why the new dyn keyword plays a key role here: without it I don’t think we should do things in this way. But now we have the ability to improve.

Furthermore, even today you can have some useless trait objects like the following:

let v:&dyn Sync = &19;
let f:&dyn FnOnce() = &||{}; //you will not be able to call this!

Having a consistent rule to lint against the above would be very useful.


#11

Yes, we could, maybe. But that would be highly surprising. If I see Trait, I want to be able to use the methods of Trait. Trait objects are about dynamic dispatch – the semantic difference between &dyn Trait and <T> where T: Trait should be just that, static vs. dynamic dispatch. Not subsetting, not the creation of a completely different type with the exact same name, not any sort of magic.


#12

I didn’t see it highly. Even today, new users are still asking why they cannot have dyn Trait on some types, simply because they are not object safe.

However, people from other languages know that some methods are not suitable for virtual dispatch, so they can accept that some functionality are not able to use for virtual dispatch.

This argument is like the two sides of a coin: on one hand, you are sure when ever you have a dyn Trait you have all functionality a Trait provides, on the other hand, you are sure that you can always virtual dispatch on a dyn Trait but only access the functionality that is suitable for virtual dispatch. Both are consistent and easy to understand. Howver, I feel like allowing more dyn Traits will make the language more flexible.

Furthermore, if we allow dyn Trait on those that is not object safe, nothing can stop us to manually do

impl Clone for Box<dyn Clone> {
    fn clone(&self) -> Self {
        Box::new((&**self).clone())
    }
}

#13

Those are two different issues. If you can’t make a non-object-safe trait into a trait object, then you get a compilation error saying that it’s not object safe. If it however compiles (for an object-safe trait), then you can be sure that your trait object does what it looks like it’s doing.

Conversely, if you allow trait objects to be formed with any trait, and restrict the methods only, you will have two things with the same name behaving differently, and you will only find out, when you have already passed a trait object down the call chain of a dozen functions, that you need a method you thought was object safe, but whoops, it isn’t.

Making the change you suggested would break local reasoning around traits in approximately the same manner C++ templates break local reasoning about generics. Rust guarantees that once the definition of a generic function with the given trait bounds compiles, it will compile, regardless of how it’s called. C++ defers this decision to post-template-instantiation time, and so if you are writing a library with a template function, you can’t really make sure it will compile and work in exactly the way you intended.

Breaking local reasoning and requiring the global one is almost always a bad thing in language design.


#14

I think it is not too hard to understand that Option<T> is not T, Box<dyn Trait>: Trait may not hold. So what make people think that dyn Trait must be Trait?

This is only true when you can also say Trait when you actually want dyn Trait. But this is going to be depreceated and I am not proposing to do it right now. Rather, once using Trait in dyn Trait position becomes hard error, it is the good time to introduce this relax.

What I am saying is: there is no absolute reason to think that dyn Trait have to be a Trait, because they are not spelled the same name. Thinking they are the same is actually an easy misunderstand for new users.

If dyn Trait grammar is required I didn’t see any local reasoning would be breaking - after all, once you have a trait object, as if you you have any objects, to do local reasoning you have to examin whether it have such a method defined, and whether the usage matches its signature. There is no difference here - if you are calling a method that is not object safe, you can see that the way it was defined is not suitable for dynamic dispatch. The compiler can also give very meaningful error messages like “This method is generic and is not suitable for dynamic dispatch”.

But I am afraid we are not talking about the same thing. Can you please give an example of how local reasoning can be breaking?

If this is what you were talking about " breaking local reasoning", I can only tell that if you called a method and it is not object safe, all you have to do is to check the defintion of the trait, and verify the method you are calling is actually object safe, just like you will need to check whether you passed a &T to a method requires &mut T. And in many cases, you will not even need to check - if the method does not have a receiver, you will not be able to call it on a trait object anyways.

A bonus is that if dyn Trait:Trait does not hold, and you have a method require T: Trait, you will not be able to pass a trait object to that function in the first place. So there is nothing can pass down.


#15

What makes people think that dyn Trait must be Trait is that dyn means “dynamic”, not “subset”. What you are proposing makes one keyword or concept actually have two distinct effects, one of which is unexpected.

Sure:

trait Trait {
    fn not_object_safe<T>(&self, x: T) {}
}

fn foo(x: &dyn Trait) {
   bar(x);
}

fn bar(x: &dyn Trait) {
    x.not_object_safe(0);
    // I only found out here that I wanted to call a method which is not object safe.
    // So now I have to change `bar` to take a generic argument instead of a trait object.
    // But that in turn requires `foo` to take a generic argument too, so this property
    // transitively affects every caller.
}

And there’s yet another hidden effect: what will happen to the other functions calling our transitive callers? They might need to be modified too, so this can induce an avalanche of changes required. This is where locality is violated.

all you have to do is to check the defintion of the trait, and verify the method you are calling is actually object safe

Again, since there will probably be several uses of a trait method, changing its signature can have wild consequences and should not be taken as trivial.


#16

If you want a workaround, have a base trait that is fully object-safe, then have an extension trait dependent on the base trait that requires Self to be Sized.


#17

We could allow dyn Trait + Sized for these types, come to think of it, it’s weird that T: Trait has Sized added by default while dyn Trait has not.


#18

No, I don’t want to have a workaround for a problem that shouldn’t exist.


#19

I’m just going to leave this here: unsafe dyn Trait, implements as much of the trait as it can. Raises a warning if unsafe dyn Trait: Trait.

I don’t have a good justification for unsafe, other than it being there to scare people into not using it, because we really, really want dyn Trait: Trait. It also makes people look at you funny for useless types like Box<unsafe dyn Clone>. Other ideas welcome.


#20

I am relying on the current behavior in one of my libraries,and would hope that if something like this were accepted that there would be a way to opt-out of dynamic dispatch.

I can’t use a Sized bound to opt-out of dyn MyTrait because I allow using str with the trait,and str is not Sized.