Create `&Trait` from `Box<T: Trait>`


#1

I would like to kindly ask for your advice about how to return a &Trait given a Box<Trait>. I spent quite some time trying to return a &Bottle at the point in the code below marked by STUMBLED HERE.

After a while I came up with the solution which I marked as Workaround.

But I still wonder if one could get rid of that workaround. Because of T: Bottle I naively thought that the compiler could create a &Bottle from the Box<T>:

struct Chardonnay { x: u32 }

trait Bottle { }

struct Shelf<T: Bottle + ?Sized> {
  bottle: Box<T>,
  // Workaround: store a raw pointer to the trait object:
  bottle_ptr: *const Bottle,
}

impl Bottle for Chardonnay { }

trait BottleOwner {
  fn bottle(&self) -> &Bottle;
}

impl<T: Bottle + ?Sized> BottleOwner for Shelf<T> {
  fn bottle(&self) -> &Bottle {

    ///////////////////////////////////////////////////////////////
    ///////////////////////////////////////////////////////////////
    // STUMBLED HERE
    // Originally, I wanted this to work:
    //return self.bottle.as_ref() as &Bottle;

    // Workaround:  Return the trait object which we saved previously:
    unsafe { &*self.bottle_ptr }
  }
}

#[test]
fn test() {
  let bottle: Box<Bottle> = Box::new(Chardonnay { x: 2014 });
  let mut shelf = Shelf {
    bottle: bottle,
    bottle_ptr: unsafe { ::std::mem::uninitialized() },
  };
  // Workaround:  Save the trait object here:
  unsafe {
    ::std::ptr::write(&mut shelf.bottle_ptr, shelf.bottle.as_ref());
  };
  shelf.bottle();
}

Thank you!


#2
trait Bottle {}

struct Shelf {
    val: Box<Bottle>
}

impl Shelf {
    fn get_ref(&self) -> &Bottle {
        &*self.val
    }
}

#3

Sorry, that doesn’t solve your problem. I was confused by your topic title which says Box<Trait> but you want Box<T> where T: Trait + ?Sized, which is not the same. But why don’t you just store a Box<Trait>?


#4

The original code can be made to work if the (unstable) unsizing feature is used - BottleOwner impl for Shelf would add the following bound on T: ... + Unsize<Bottle>.

That would allow T: Bottle to be used (i.e. a sized type implementing Bottle) or a &Bottle/Box<Bottle> trait object already.

But, you likely don’t want to go down this path in your code. Perhaps you can explain why you’re trying to mix generics, trait objects, and ?Sized constraints? :slight_smile:


#5

One would expect the mere existence of a Box<T> to be a witness to the fact that T must be either sized or Bottle, but it seems the compiler is unable to deduce that.

If you don’t mind the duplication, you could instead write:

impl BottleOwner for Shelf<Bottle> {
  fn bottle(&self) -> &Bottle {
    &*self.bottle
  }
}

impl<T: Bottle + Sized> BottleOwner for Shelf<T> {
  fn bottle(&self) -> &Bottle {
    &*self.bottle
  }
}

Alternatively, you could also tweak your trait so that it can as_bottle itself:

trait AsBottleRef {
  fn as_bottle(&self) -> &Bottle;
}

impl<T: Bottle> AsBottleRef for T {
  fn as_bottle(&self) -> &Bottle { self }
}

trait Bottle: AsBottleRef {}

#6

this might work out

use std::borrow::Borrow;
impl<T: Bottle + ?Sized> BottleOwner for Shelf<T>
where
    Box<T>: Borrow<Bottle>,
{
    fn bottle(&self) -> &Bottle {
        self.bottle.borrow()
    }
}

#7

The problem is that the types substituable for T are virtually unbounded. Moreover, T is unsized in the bound. That T can therefore be another trait object. But if it’s another trait object, the concrete type is erased. Since that’s erased, you can’t form a fat pointer to &Bottle because you can’t find the vtbl for it. That’s where Unsize helps as an additional constraint.

So, this isn’t so much about type checking, but rather the way trait objects are represented doesn’t work in this case.


#8

You’re right! There could be other trait objects that satisfy Bottle, so a Box<T: Bottle + !Sized> does not imply T == Bottle.


#9

Yeah, exactly. It could also be a slice, another unsized type. But the root issue is that Box<T: Trait + ?Sized> is very different from Box<Trait>. In the former, Trait is just a static bound - there’s not necessarily runtime representation, so to speak. Compiler monomorphizes the calls to T’s Trait impl at compile time and that’s it - if the type was unsized (and not already a Trait trait object, which is what Unsize allow to constrain this to be), there’s no way to “recover” the vtbl for Trait impl of that type.


#10

Thank you for your help! My updated and working example is at the end of this post.

My interface had already the

trait BottleOwner {
  fn bottle(&self) -> &Bottle;
}

so I thought first that I could have the best of both worlds if Shelf could still know the conrete type T but still be able to return a &Bottle.

Right, bringing ?Sized into the mix was maybe a bad idea. I wanted to allow Shelf to handle both concrete types as well as trait objects, so accepting either T == Chardonnay as well as T == Bottle so that the

BottleOwner::open(&self) -> ()

which I just now added in the example could either use static or dynamic dispatch. But not sure if that will be so useful in the overall design.

This works great, thanks! I should read more about this new feature…

Thank you! Yes, maybe I can hide that in a macro.

I see, so instead of spending memory by keeping the correct vtable ptr in my bottle_ptr I spend another virtual call into the trait object which retrieves the vtable at runtime.

Ah, so in Shelf, if T is a trait, then the compiler has forgotten where the vtable for Bottle is even though T: Bottle, do I understand this right?

But how does the Unsize bound help then? Does it keep the vtable of Bottle around?

Thank you! Yes, that compiles fine as well, great!

Here is the updated example with Shelf using the Unsize approach and Cupboard using the Borrow approach:

trait Bottle {
  fn open(&self);
}

trait BottleOwner {
  fn bottle(&self) -> &Bottle;
  fn open(&self);
}

struct Chardonnay {
  x: u32,
}

impl Bottle for Chardonnay {
  fn open(&self) {}
}

struct Shelf<T: Bottle + ?Sized> {
  bottle: Box<T>,
}

impl<T: Bottle + ?Sized + ::std::marker::Unsize<Bottle>> BottleOwner for Shelf<T> {
  fn bottle(&self) -> &Bottle {
    // Really nice, with `Unsize<Bottle>` bound, this works :-)
    self.bottle.as_ref() as &Bottle
  }

  fn open(&self) {
    // Would this use static dispatch if T is a struct, and vtable if T is a
    // trait?
    self.bottle.open();
  }
}

// A second example to try the suggestion to use `Borrow`:
struct Cupboard<T: Bottle + ?Sized> {
  bottle: Box<T>,
}

impl<T: Bottle + ?Sized> BottleOwner for Cupboard<T>
where
  Box<T>: ::std::borrow::Borrow<Bottle>,
{
  fn bottle(&self) -> &Bottle {
    use std::borrow::Borrow;
    self.bottle.borrow() as &Bottle
  }

  fn open(&self) {
    self.bottle.open();
  }
}

#[test]
fn test() {
  let shelf = Shelf {
    bottle: Box::new(Chardonnay {
      x: 2014,
    }) as Box<Bottle>,
  };
  let owner: &BottleOwner = &shelf;
  owner.bottle();
  owner.open();

  // Try also with Cupboard which uses the Borrow method:
  let cupboard = Cupboard {
    bottle: Box::new(Chardonnay {
      x: 2014,
    }) as Box<Bottle>,
  };
  let owner: &BottleOwner = &cupboard;
}

#11

Yeah, that’s right. Since generics monomorphize at compile time, the compiler knows which functions to call on the trait that implements Bottle. But, the vtbl isn’t persisted anywhere (this is a big difference between say Java, where all types carry a vtbl no matter what, and Rust where only trait objects carry a vtbl, and only for the trait it represents to boot).

Unsize is a marker trait, similar to Send and Sync. As such, its purpose is to only restrict the set of valid types that can be substituted for T. In particular, it only allows T: Bottle + Sized, T: &Bottle, and Box<Bottle>. So in the first case, everything is good - T is a sized type that impls Bottle, and you can form a trait object off it cause the type isn’t erased. In the latter two, the type is erased but we already have a Bottle trait object so the vtbl is there.