Why do we need object safety rules?

Any trait with object-unsafe methods can be made object safe by adding where Self: Sized to all of the object-unsafe methods.

// this trait is not object-safe
trait Clone {
    fn clone(&self) -> Self;
}

// this one is
trait Clone {
    fn clone(&self) -> Self where Self: Sized;
}

I don't know why it's like this, though, rather than considering all traits to be object safe.

4 Likes

As I said, I understand, why certain methods cannot be called on trait objects, but why shouldn't I be able to to call baz below?

trait MyTrait {
    fn foo() -> Self;
    fn bar(&self) -> Self;
    fn baz(&self) -> &str; // Object-safe!
}

In other words, why restrict trait object creation, rather than not allowing to call object-unsafe methods?

Because the signature of MyTrait::bar says all MyTraits have one but dyn MyTrait doesn't.
And Rust is very careful with hidden things on signatures (AFAIK only lifetime elision).

Actually that means that the type dyn MyTrait has an item bar. There is no syntax to refer to bar without also specifying the type. So MyType::bar or <MyType as MyTrait>::bar. But with trait objects, we don't have this type info, so we can't call these functions even if we wanted to.

Because it wants to have the rule that dyn Foo: Foo -- the opposite, where a trait object doesn't always implement its trait, is just weird.

8 Likes

Related discussion here: Does object safety prevent any legitimate use cases?

This may be more pleasing from a language-theoretical point of view, but on the other hand it's pretty annoying that I can't have a Vec<Box<dyn Foo>> just because Foo has some object-unsafe method (which I had no intention of using anyway). I might even call this weird.

I'd prefer that the compiler refused to coerce dyn Foo as Foo if and when I actually asked for that, rather than stopping me earlier just because this situation might arise.

1 Like

This seems to be counter to Rust's philosophy. Rust wants to prevent problems as early as possible, so

  • we add bounds to generics instead of duck-typing in order to guarantee that we get the desired behavior
  • we have a conservative lifetime checker to guarantee validity of references
  • finally, we don't allow you to create trait objects that are not object safe to guarantee that you don't lose any behavior in the process of converting T to dyn Foo.

The common theme is that Rust tries to be conservative when there are a number of options, and making dyn Foo: Foo seems to be in line with that.

2 Likes

I wonder if the best way about this is to add an additional bound to all the trait objects + asFoo, where there's a trait asFoo which has a method you can call that returns the Foo itself.

So something like the following:

trait Foo {
    fn clone_self(&self /*Ok*/) -> Self; /*Not ok*/
}
impl Foo for u32 { /**/ }
let x: Box<dyn Foo> = Box::new(31415u32); //Say this was okay.
fn bar<T: Foo + ?Sized>(value: &T) {
    value.clone_self();
}
foo(&*x);

Which makes sense from a language theoretical standpoint, if I have a T: Foo, surely I should be able to use a function Foo::fn(&self) -> _, but then we impede with T: Foo + ?Sized which would mean that <Foo + ?Sized>::fn(&self) -> Self makes no sense, so I guess this would contradict the case of there being a dyn Foo created or used.

Unless we use generic constraints as previously mentioned:

trait Foo {
    fn clone_self(&self) -> Self where Self: Sized;
}
foo(&*x);

This would fail to compile because dyn Foo: !Foo. It is weird, but should be doable. (I am against this because it seems like it would be a source of confusion if added)

I'm sorry, I don't quite understand, do you mean dyn AnyKindOfTrait: !AnyKindOfTrait? Or dyn NotObjectSafeTrait: !NotObjectSafeTrait? Because in the case we try the following:

trait Foo {}
impl Foo for dyn Foo {}

The compiler complains about dyn Trait inherently having Trait implemented for it.

This one, this is hypothetical right now

1 Like

I'm curious, is it possible to make (probably through the procedural macro) a "wrapper-trait", which implements only object-safe methods of some other trait by delegating to their original implementation and which is implemented always whether the original trait is? It seems that this will remove the confusion: if you don't need the non-object-safe methods - use the wrapper, and all is OK. The only issue I can see now is the name conflicts when both traits are in scope (and, of course, the very possibility of the auto-generation - without it this is not very feasible).

1 Like

Yeah, I think that would be a good way of solving this problem! To solve the naming issue, we could just supply a different name from the trait, probably let the user of this proc-macro select the name.

I was thinking not about the trait name, but about the method names. Check this (playground):

trait NonObjectSafe {
    fn non_safe() -> Self;
    fn safe(&self);
}

trait ObjectSafe {
    fn safe(&self);
}

impl<T> ObjectSafe for T where T: NonObjectSafe {
    fn safe(&self) {
        <Self as NonObjectSafe>::safe(self)
    }
}

impl NonObjectSafe for u32 {
    fn non_safe() -> Self {
        1
    }
    fn safe(&self) {
        println!("I'm {}. Object safe methods FTW!", self);
    }
}

fn main() {
    0u32.safe(); // error[E0034]: multiple applicable items in scope
}
2 Likes

Oh, I see what you mean now. The only solution seems to be documenting that both the NonObjectSafe trait and the ObjectSafe trait shouldn't be brought into scope at the same time for ergonomic reasons. Althoough, this doesn't seem to be a problem for generic functions so this may not be as bad as it seems. Just import the NonObjectSafe where you need it, and you can use the fully qualified name for the ObjectSafe version (which should be more rare, so this verbosity should be manageable)

1 Like

Well, sometimes it does and other times it doesn't. Look at Send and Sync for example: the compiler could require you to derive these traits explicitly, but that'd be too annoying, so it doesn't. I might also point to stuff like trailing return values and even type inference in general. The choice between implicit vs explicit is made on a case-by-case basis. I don't think there exists some widely agreed "Rust philosophy" on these matters.

dyn Trait : Trait is more useful than just a theoretical point of view: if we had dyn NotObjectSafe : !NotObjectSafe, then we would be doomed not to be able to have a generic function accepting both impl NotObjectSafe + Sized and dyn NotObjectSafe, even if the body of the function only used the object safe methods of the trait. It would be quite confusing and could lead to using macros instead of generics, for the syntaxic "duck typing" they provide.


The idea of the proc_macro_attribute is quite interesting, I may work on that this week-end: the attribute would add where Self : Sized bounds on each method that is not object-safe, so as to solve the OP's complaint about ergonomics, while also solving this issue about genericity: impl NotObjectSafe + ?Sized would accept both kinds of inputs, and would forbid using the methods with the auto-added where Self : Sized bound.

4 Likes

That looks even better then the thing I've started myself (with wrapper trait) :slight_smile: I'll try to do it anyway (even if just as an exercise).

The only issue I could see in your case is if the implementor type is DST itself - the "non-object-safe" methods could be perfectly useful for it (for example, the associated function returning Box<Self>), but they'll be banned by this attribute.

2 Likes