Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How does the mechanism behind the creation of boxed traits work?

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?
like image 429
Askaga Avatar asked Sep 12 '18 06:09

Askaga


2 Answers

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

  • Why can't I write a function with the same type as Box::new?
  • Are polymorphic variables allowed?
  • How do you actually use dynamically sized types in Rust?
  • Why is `let ref a: Trait = Struct` forbidden?
like image 76
trent Avatar answered Nov 14 '22 03:11

trent


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).

like image 28
rodrigo Avatar answered Nov 14 '22 04:11

rodrigo