15

I'm having trouble understanding how values of boxed traits come into existence. Consider the following code:

trait Fooer {
    fn foo(&self);
}

impl Fooer for i32 {
    fn foo(&self) { println!("Fooer on i32!"); }
}

fn main() {
    let a = Box::new(32);                        // works, creates a Box<i32>
    let b = Box::<i32>::new(32);                 // works, creates a Box<i32>
    let c = Box::<dyn Fooer>::new(32);           // doesn't work
    let d: Box<dyn Fooer> = Box::new(32);        // works, creates a Box<Fooer>
    let e: Box<dyn Fooer> = Box::<i32>::new(32); // works, creates a Box<Fooer>
}

Obviously, variant a and b work, trivially. However, variant c does not, probably because the new function takes only values of the same type which is not the case since Fooer != i32. Variant d and e work, which lets me suspect that some kind of automatic conversion from Box<i32> to Box<dyn Fooer> is being performed.

So my questions are:

  • Does some kind of conversion happen here?
  • If so, what the mechanism behind it and how does it work? (I'm also interested in the low level details, i.e. how stuff is represented under the hood)
  • Is there a way to create a Box<dyn Fooer> directly from an i32? If not: why not?
Aplet123
  • 33,825
  • 1
  • 29
  • 55
Askaga
  • 6,061
  • 5
  • 25
  • 49
  • I ask myself, why you want to have a `Box`. I think you don't quite understand, how traits work in Rust. Try to call `.foo()` on every variable (`a..e`) and see, what happens. – hellow Sep 12 '18 at 07:07
  • 2
    The compiler tells you the exact reason, why `c` does not work: `the method 'new' exists but the following trait bounds were not satisfied: 'Fooer : std::marker::Sized'` – hellow Sep 12 '18 at 07:08
  • This is another question about variance. I am sorry, but I cannot help: this kind of stuff still triggers me :P – Boiethios Sep 12 '18 at 07:32
  • @hellow: Not sure what you're getting at. I'm just experimenting/learning so there is no particular reason why i "want to have" a `Box`. Surely there are plenty of sensible applications for it otherwise the functionality wouldn't exist. Also what do you mean by "try to call `.foo()` on every variable `a..e`"? It always prints the same thing, as expected. – Askaga Sep 12 '18 at 07:57
  • 1
    @hellow: about the error message: it seems to imply that it actually cannot call `Box::::new` since the given type parameter must have as size known at compile time which is not the case since `Fooer` is a trait, right? So my assumption is correct that you must construct a Box of a sized type first, than convert it to Box of the trait type somehow, which would leave me with my original question: how/by what mechanism is that conversion happening? – Askaga Sep 12 '18 at 08:02
  • I wanted to say, that there is no need to have an explicit `Box`, because it doesn't matter. There is no offense in my comment at all. As you say yourself, they all print the same thing, so there is no difference whether you have a `Box` or whatever, you can call the trait method on it, nevertheless. – hellow Sep 12 '18 at 08:02
  • Maybe [the book](https://doc.rust-lang.org/1.8.0/book/trait-objects.html) can help you to understand trait-objects and how dispatching in rust works. – hellow Sep 12 '18 at 08:04
  • [The Rustonomicon](https://doc.rust-lang.org/beta/nomicon/subtyping.html) has a good section about variance as well. – hellow Sep 12 '18 at 08:12
  • And this [other chapter in the Rustonomicon](https://doc.rust-lang.org/nomicon/coercions.html) talks about unsized coertions, that I think are at play here. – rodrigo Sep 12 '18 at 09:31
  • 1
    @Boiethios AFAIU Variance is confined to lifetimes in Rust, since there is no subtyping relationship between a trait and an implementing struct. There is no "structural inheritance". Otherwise, given that `Box` is variant over `T` I could cast a `Box>` to `Box>`. On the other hand, casting a `Box>` to `Box>` where `'a : 'b` is possible, because `'a : 'b` is an actual subtyping relationship unlike `i32: Fooer` and an immutable borrow `&'x T` is not only variant over `T` but also over `'x` . – Calculator Sep 12 '18 at 13:42
  • @Boiethios Sorry, I made a typo. Casting `Box>` to `Box>` is possible. `Box>` to `Box>` is of course not possible for the same reason `Box>` to `Box>` is not possible. – Calculator Sep 12 '18 at 14:24

2 Answers2

21

However, variant c does not, probably because the new function takes only values of the same type which is not the case since Fooer != i32.

No, it's because there is no new function for Box<dyn Fooer>. In the documentation:

impl<T> Box<T>

pub fn new(x: T) -> Box<T>

Most methods on Box<T> allow T: ?Sized, but new is defined in an impl without a T: ?Sized bound. That means you can only call Box::<T>::new when T is a type with a known size. dyn Fooer is unsized, so there simply isn't a new function to call.

In fact, that function can't exist in today's Rust. Box<T>::new needs to know the concrete type T so that it can allocate memory of the right size and alignment. Therefore, you can't erase T before you send it to Box::new. (It's conceivable that future language extensions may allow functions to accept unsized parameters; however, it's unclear whether even unsized_locals would actually enable Box<T>::new to accept unsized T.)

For the time being, unsized types like dyn Fooer can only exist behind a "fat pointer", that is, a pointer to the object and a pointer to the implementation of Fooer for that object. How do you get a fat pointer? You start with a thin pointer and coerce it. That's what's happening in these two lines:

let d: Box<Fooer> = Box::new(32);        // works, creates a Box<Fooer>
let e: Box<Fooer> = Box::<i32>::new(32); // works, creates a Box<Fooer>

Box::new returns a Box<i32>, which is then coerced to Box<Fooer>. You could consider this a conversion, but the Box isn't changed; all the compiler does is stick an extra pointer on it and forget its original type. rodrigo's answer goes into more detail about the language-level mechanics of this coercion.

Hopefully all of this goes to explain why the answer to

Is there a way to create a Box<Fooer> directly from an i32?

is "no": the i32 has to be boxed before you can erase its type. It's the same reason you can't write let x: Fooer = 10i32.

Related

trent
  • 25,033
  • 7
  • 51
  • 90
11

I'll try to explain what conversions (coercions) happen in your code.

There is a marker trait named Unsize that, between others:

Unsize is implemented for:

  • T is Unsize<Trait> when T: Trait.
  • [...]

This trait, AFAIK, is not used directly for coercions. Instead, CoerceUnsized is used. This trait is implemented in a lot of cases, some of them are quite expected, such as:

impl<'a, 'b, T, U> CoerceUnsized<&'a U> for &'b T 
where
    'b: 'a,
    T: Unsize<U> + ?Sized,
    U: ?Sized

that is used to coerce &i32 into &Fooer.

The interesting, not so obvious implementation for this trait, that affects your code is:

impl<T, U> CoerceUnsized<Box<U>> for Box<T> 
where
    T: Unsize<U> + ?Sized,
    U: ?Sized

This, together with the definition of the Unsize marker, can be somewhat read as: if U is a trait and T implements U, then Box<T> can be coerced into Box<U>.

About your last question:

Is there a way to create a Box<Fooer> directly from an i32? If not: why not?

Not that I know of. The problem is that Box::new(T) requires a sized value, since the value passed is moved into the box, and unsized values cannot be moved.

In my opinion, the easiest way to do that is to simply write:

let c = Box::new(42) as Box<Fooer>;

That is, you create a Box of the proper type and then coerce to the unsized one (note it looks quite similar to your d example).

Shepmaster
  • 388,571
  • 95
  • 1,107
  • 1,366
rodrigo
  • 94,151
  • 12
  • 143
  • 190