Specifying that trait should be object-safe for purpose of casting to object


#1

I have some traits that are expected to be dynamically polymorphic and used as objects. I want to provide methods for them. Something like

fn use_trait(x: &Trait) {
    println!("object says {}", x.needed());
}

trait Trait {
    fn needed(&self) -> &str;

    fn provided(&self) {
        use_trait(self);
    }
}

struct Struct();

impl Trait for Struct {
    fn needed(&self) -> &str {
        "Hello, world!"
    }
}

fn main() {
    Struct().provided();
}

Except that

        use_trait(self);

does not compile, because it turns out that Trait is not guaranteed to be object-safe here. And fixing it by requiring Sized won’t work, because the trait is to be used as &Trait most of the time and requiring Trait : Sized or needed() where Self : Sized would both break that.

So I asked over on SO some time ago and managed to buidl this solution:

fn use_trait(x: &Trait) {
    println!("object says {}", x.needed());
}

trait Trait : AsTrait {
    fn needed(&self) -> &str;

    fn provided(&self) where Self : AsTrait {
        use_trait(self.as_trait());
    }
}

trait AsTrait {
    fn as_trait(&self) -> &Trait;
}

impl<T : Trait + Sized> AsTrait for T {
    fn as_trait(&self) -> &Trait { self }
}

struct Struct();

impl Trait for Struct {
    fn needed(&self) -> &str {
        "Hello, world!"
    }
}

fn main() {
    Struct().provided();
}

Now this works, but it is Ugly™. Worse, I could not get this to work:

fn use_trait(x: &Trait) {
    println!("object says {}", x.needed());
}

trait Trait : AsObj<Trait> {
    fn needed(&self) -> &str;

    fn provided(&self) where Self : AsObj<Trait> {
        use_trait(self.as_obj());
    }
}

trait AsObj<T> {
    fn as_obj(&self) -> &T;
}

impl<Trait, Type : Trait + Sized> AsObj for Type {
    fn as_obj(&self) -> &Trait { self }
}

struct Struct();

impl Trait for Struct {
    fn needed(&self) -> &str {
        "Hello, world!"
    }
}

fn main() {
    Struct().provided();
}

because the compiler didn’t like the declaration

trait Trait : AsObj<Trait>

In C++, this is common idiom. In fact, it is so common that it derives it’s name, curiously recurring template pattern, from the fact it is common rather than from what it does.

Anyway, my questions are:

  1. Is there a simpler way to solve this?
  2. And if there is not, should there be?

Without ability to specify that it shall be possible to cast &Self to &Trait, using dynamic polymorphism for something more complicated gets rather unwieldy.


Polymorphism with traits
#2

You could provide the Self: ?Sized (un)constraint on your stand-alone use_trait function:

fn use_trait<T: Trait + ?Sized>(x: &T) {
    println!("object says {}", x.needed());
}

and then it compiles just fine: playground.


#3

Well, I got a variation of this also working for you—AsObj is not generic on Trait here—but you could probably do that. The tricks are to

  1. implement AsObj for references to your traits/structs so that your Self is Sized, and
  2. implement AsObj for &Trait instead of requiring trait Trait: AsObj<Trait> which doesn’t seem to be strictly necessary (conceptually, &Trait should always be coercible to &Trait without explicitly extending AsObj).

Playground
Edit 3: Playground Version with Obj<Trait> as well.

However, I haven’t quite wrapped my mind around why the original error regarding Sized even happens. It’s probably due to the same thing, but I oughta just go to bed now. :wink:

Edit: Aaaaah, I can’t sleep because of this! I simplified the playground example above to really understand the Sized constraint, and it feels like a bug or shortcoming in the compiler. Here’s the issue that lays out the bare minimum, but the devs may provide some insights as to the genericity of unsized types.

Edit 2: Failing to sleep even more, I recalled this thread where I learned about the as-yet unstable Unsize marker trait which serves the same purpose as your AsObj trait, allowing you to support both Sized types implementing &Trait and trait objects. Et voilĂ : playground.

Edit 3: Of course, in the morning, it aaaaaall makes sense now. Someone else pointed the same thing out on the issue as well. Trait objects consist of a pair of thin pointers—one to the vtable and one thin pointer to the object itself. Unfortunately, DSTs are fat pointers, so that when trying to convert, e.g., [Type] (fat pointer) to &Trait, you need the trait object to be a superfat pointer (at least 3-pointer-sized), which rust doesn’t currently support. A special case is &Trait → &Trait which doesn’t need any conversion to a trait object as it’s already one. Unsize<Trait> handles both the convertible cases (Type: Trait + Sized and &Trait).


#4

is this what you were trying to do? It works on the playpen.

fn use_trait(x: &Trait) {
    println!("object says {}", x.needed());
}

trait Trait {
    fn needed(&self) -> &str;

    fn provided(&self)
    where
        Self: Sized,
    {
        use_trait(self);
    }
}

struct Struct();

impl Trait for Struct {
    fn needed(&self) -> &str {
        "Hello, world!"
    }
}

fn main() {
    let s = Struct();
    use_trait(&s); // &Struct can be used as a trait object (&Trait)
    s.provided(); // you can use the default method too!
}


#5

Why does provided need Self: Sized if it only ever uses Self by reference (self: &Self)? In this case, it would specialize to

fn provided(self: &Trait) {
    // self is &Trait, use_trait() takes &Trait
    use_trait(self);
}

#6

This is because you can’t coerce from one trait object type to another. So if you had code like this:

trait OtherTrait { ... }
impl Trait for OtherTrait { ... }

then calling Trait::provided on an &OtherTrait value would not work because there is no way to cast from &OtherTrait to &Trait. (The reason this cast isn’t possible is related to how fat pointers to trait objects are implemented in Rust. There’s currently no way to get the appropriate vtable pointer for Trait given only an &OtherTrait fat pointer.)


#7

There’s an old-ish, but I think still relevant (at least in this aspect), blog post by @nikomatsakis that goes over this stuff. The section that’s particularly relevant is http://smallcultfollowing.com/babysteps/blog/2014/01/05/dst-take-5/#conversion-rules. To quote part of it:

The second rule Fat-Object permits a pointer to some type T to be coerced into an object type for the trait Trait. This rule has three conditions. The first condition is simply that T must implement Trait, which is fairly obvious. The second condition is that T itself must be sized. This is less obvious and perhaps a bit unfortunate, as it means that even if a type like [int] implements Trait, we cannot create an object from it. This is for implementation reasons: the representation of an object is always (pointer, vtable), no matter the type T that the pointer points at. If T were dynamically sized, then pointer would have to be a fat pointer – since we do not known T at compile time, we would have no way of knowing whether pointer was a thin or fat pointer. What’s worse, the size of fat pointers would be effectively unbounded. The final condition in the rule is that the type T has a suitable alignment; this rule may not be necessary. See Appendix A for more discussion.


#8

Works if use_trait is a stand-alone function. If it is a method of another dynamically polymorphic object, it does not, because methods of dynamically polymorphic objects can’t be generic over types.

If it works with constraint on the method only, then

is way easier.

Now does that actually work? This

proves nothing, because s is Struct, while the code I need this for only ever has Arc<Trait>, which derefs to &Trait and Trait : !Sized, so the where is not satisfy. So the question is, does the where apply to the existence of the method, or only to the provided implementation … https://is.gd/iqsaTb … no, that does not work!. provided() must still be part of the Trait and it is not.

Ok. Turns out the two are not equivalent.

https://is.gd/Zc6lIe

DOES, in fact, work, because this other where is satisfied for Trait. I suspect it could be simplified further, but at least it is generic, so I don’t need separate helper for each of the ~7 traits and the implementor still does not need to care about it anyway.

But then, the interesting thing is

I didn’t even know that was possible. Turns out there was nothing to know, because it does not compile. You can declare self:type, but type can only be Self, &Self or &mut Self, not &Trait.

So what is left is the

Which, indeed, does work—modified playground that actually calls provided on a trait reference as was the original intent that I somehow forgot to explicitly express in the original sample. And the quirky helper trait until Unsized gets stabilized.

And since the Unsized already exists, the answer to

is clearly, yes, it should and will.

Thanks everybody.


#9

Ok, one more thing. The whole point of putting provided in the trait is that it still can be overridden. Which it turns out it can: https://is.gd/MNQl3P. The disadvantage is that the implementation has to copy the whole ugly signature

fn provided<'a>(&'a self) where &'a Self: AsObj<Trait>

verbatim, even though it does not really make much sense at that point.


Fortunately it turns out those references are not necessary. The method can be simplified to just

fn provided(&self) where Self: AsObj<Trait>

which is significantly less ugly than with all those pointless lifetimes. And it should, since the Unsize version does not need the lifetimes either. So the only problem is that trying to move the constraint to the trait itself, as either

trait Trait: AsObj<Trait>

or

trait Trait where Self: AsObj<Trait>

fails with

error[E0391]: unsupported cyclic reference between types/traits detected

and therefore the constraint has to be repeated on all the provided methods. And, um, wait a second… with the lifetimes eliminated, it actually does not need to be repeated in the implementations!.

So, I guess that does the trick. Thanks again!


#10

Very nice! Good job cleaning up the last bit of it and getting rid of the reference and the need for repeating the method constraint!


#11

You’ve missed “it would specialize to” part. What followed what a pseudo-syntax. Replace self with any valid name (self is a keyword) to make it compile. It also needs to be a free-standing function, not a trait method.


#12

Ah, ok. Well, that pseudo-syntax was wrong then. Because it would not specializeinstantiate to self: &Trait—there is huge difference between &Trait and &T where T : Trait. The later can’t be coerced to the former unless T : Sized. That’s just a fact of Rust. It is also explained in the SO question I linked in the first post—it is quite long, so I didn’t want to copy it over in its entirety.


#13

Ah, right, instantiate – thanks for the correction. No, I didn’t mean &T where T : Trait, I meant the impl Trait for Trait, which would look somewhat like this (again, pseudo-syntax):

impl Trait for Trait {
    fn needed(self: &Trait) -> &str {
        // indirect call via the vtable
        self.needed()
    }
    // this is not actually overriding the default implementation,
    // I'm just writing down how it instantiates for Self = Trait
    fn provided(self: &Trait) {
        // self is &Trait, use_trait() takes &Trait
        use_trait(self);
    }
}

Thanks, that’s an interesting gotcha (of the kind that should absolutely be documented).

So, if I’m getting this right, the instantiation for Self = Trait (impl Trait for Trait) would indeed work just fine, but the one with Self = OtherTrait (for impl Trait for OtherTrait) would not. Because the code in generic impls, including trait default methods that are implicitly generic over Self, is verified in its generic form (in contrast with SFINAE), we can’t have that method.

Now it all clicks, thank you!


#14

What should that do?

A trait is a type class, not a type. Implementing traits for traits does not make sense. And for good reason: if Type : Trait and there are impl Trait for Type and impl Trait for Trait, which one gets used for t as &Trait where t : &Type?

Is that, actually, valid syntax? I don’t think so. You can do

trait OtherTrait { ... }
impl<T> Trait for T : OtherTrait { ... }

which is slightly different—you are implementing Trait for all types that implement OtherTrait, not for OtherTrait itself.

And yes, given o : &OtherTrait, it is still not possible to do o as &Trait. I believe it is possible if you

trait OtherTrait : Trait { ... }
impl<T> Trait for T : OtherTrait { ... }

#15

“impl trait for Trait” is actually a thing (and an official name for it), mentioned for example here.

No, if you have trait Trait, then Trait means both the trait and the Trait type (of trait objects). You almost never see the Trait type directly – usually you see &Trait, Box<Trait> or something.

“impl Trait for Trait” means that objects of type Trait (trait objects) automatically implement the Trait trait. Here’s an example (https://is.gd/Kyc1nX):

use std::fmt::Debug;

fn generic<T: Debug + ?Sized>(t1: &T, t2: &T) {
    println!("generic {:?} {:?}", t1, t2);
}

fn trait_objects(t1: &Debug, t2: &Debug) {
    println!("trait objects {:?} {:?}", t1, t2);
}

#[derive(Debug)]
struct Struct;

fn main() {
    // generic function, static dispatch:
    // T = i32
    generic(&35, &42);
    // T = Struct
    generic(&Struct, &Struct);

    // dynamic dispatch, can take
    // trait objects of originally
    // different types at the same time
    trait_objects(&35, &Struct);

    // and now, impl Trait for Trait
    // it's generic::<Debug>()
    generic(&35 as &Debug, &Struct as &Debug);
}

impl Trait for Trait works automatically (you don’t have to manually write the impl). What I’ve written above was, again, some pseudo-syntax to describe how the implicit implementation looks.

You can, however, manually implement a trait for another trait (that is, for trait objects of type that other trait):

trait T1 {
    fn foo(&self);
}

trait T2 {
    fn bar(&self);
}

impl T1 for T2 {
    fn foo(&self) {
        println!("impl T1 for T2");
        self.bar();
    }
}

impl T2 for i32 {
    fn bar(&self) {
        println!("impl T2 for i32: {}", *self);
    }
}

fn main() {
    (&42 as &T2).foo();
}

https://is.gd/RuQ54I


#16

Yes. As long as the type actually exists (or is instantiable; I am not sure, but it does not really matter). Notably for this discussion, if Trait : Sized, then Trait is not (usable as) type—and the implicit impl Trait for Trait does not get generated.

Anyway, premise of the original question is that objects of type Trait do indeed implement Trait. However, the coercion from &T where T : Trait, to &Trait requires T : Sized, which makes it impossible to provide default implementation of method that need to pass &Trait on.

I wanted to say that I don’t see a point for doing that over doing impl<T: T2> T1 for T except the solution above does exactly that. Because it does:

 impl AsObj<Trait> for Trait { ... }
 impl<T: Trait> AsObj<Trait> for T { ... }

which is actually equivalent to

 impl AsObj<Trait> for Trait { ... }
 impl<T: Trait + Sized> AsObj<Trait> for T { ... }

because generic parameters are implicitly : Sized except when specified otherwise.

The cast from &OtherTrait to &Trait is not possible even if either impl Trait for OtherTrait or impl<T: OtherTrait> Trait for T exist (I think it should be possible if OtherTrait : Trait), but fortunately that is not needed here. And it is not too interesting as it is just a special case of T: !Sized.


#17

I had pretty similar requirements and found a nightly-compatible solution that seems to me like a very concise mapping of what I’m conceptually trying to achieve. I uses specialization instead of a default implementation:

#![feature(specialization)]

trait User {
    fn use_trait(&self, &Trait);
}

struct SimpleUser;
impl User for SimpleUser {
    fn use_trait(&self, object: &Trait) {
        print!("User: ");
        object.needed();
    }
}

trait Trait {
    fn needed(&self);
    fn provided(&self, user: &User);
}

default impl<T: Trait + Sized> Trait for T {
    // FIXME: This dummy implementation of `needed` can go when
    // https://github.com/rust-lang/rust/issues/37653 is merged
    fn needed(&self) {
        self.needed()
    }
    fn provided(&self, user: &User) {
        user.use_trait(self)
    }
}

struct SimpleTrait;
impl Trait for SimpleTrait {
    fn needed(&self) {
        println!("SimpleTrait");
    }
}

struct ListTrait {
    objects: Vec<Box<Trait>>,
}
impl Trait for ListTrait {
    fn needed(&self) {
        println!("ListTrait");
    }
    fn provided(&self, user: &User) {
        for object in &self.objects {
            print!("List item: ");
            user.use_trait(&**object);
        }
    }
}

fn main() {
    let objects: [Box<Trait>; 2] = [
        Box::new(ListTrait { objects: vec![Box::new(SimpleTrait)] }),
        Box::new(SimpleTrait),
    ];
    let runner = SimpleUser;
    for s in &objects {
        s.needed();
        s.provided(&runner);
    }
}

Playground. Since I want the library to be compatible with stable, I currently copy the default implementation to every impl, but it’s nice to see that there will be a nice way to do this in the future.