Deref issue for Deref<Target = Box<dyn Trait>>

Hi I'm having trouble wrapping my head around why the compiler is not able to cast / coerce on the last line of this contrived example. I've made this so that I can understand what is going on here with the compiler as opposed to directly solving a real world problem:

use std::{ops::Deref, cell::{RefCell, Ref}, any::Any};

fn main() {

    let refcell: RefCell<Box<i32>> = RefCell::new(Box::new(1));
    let ref_: Ref<Box<i32>> = refcell.borrow();
    let boxed_ref = Box::new(ref_);

    //let trait_obj: Box<dyn Deref<Target = Box<i32>>> = boxed_ref; // Works if inner type is concrete
    let trait_obj: Box<dyn Deref<Target = Box<dyn Any>>> = boxed_ref; // Won't work if inner type is a trait object
}

Also on playground

If the second last statement with the concrete type is uncommented instead of the last there is no issue. So the problem is with using a trait object vs using a concrete type at that location. I've checked that rust has a CoerceUnsized implementation for cell::Ref so I'm not sure why this is not working.

Thanks!

Ref<Box<i32>> is Deref<Target = Box<i32>>, it is not Deref<Target = Box<dyn Any>>. The coercion is impossible. You couldn't even write it manually if you wanted to, because Ref<Box<i32>> doesn't have a Box<dyn Any> to borrow from.

Even without the trait object, just because you can coerce Box<i32> to Box<dyn Any> does not mean you can coerce Ref<Box<i32>> to Ref<Box<dyn Any>>. Unsizing coercions are not transitive through arbitrarily many layers of reference.

1 Like

The way to understand this is to observe that there isn't anything special about Box or dyn Trait in this behavior. It's the same reason why you can't return arbitrary smart pointer types from Index or Borrow, for instance:

Box is an owning container. The coercion to Box<T> to Box<dyn Trait> is not some sort of subtyping relationship; it's a proper type conversion, involving type erasure, which results in two values of different types and different layouts (Box<Sized> is a regular old pointer, whereas Box<!Sized> is a fat pointer). It is only possible to perform it implicitly because Box (more precisely, the Unsize trait and related mechanisms) are hard-wired into the language. But other than convenience (and some ugliness/impurity if you view it from a language design point of view), this has no far-reaching consequences. It's just like invoking a function that takes a Vec<i32>, converts every number to floating-point, and returns a Vec<f64>. The result still represents the same numbers, but with a completely different memory layout.

Now, can you write a (useful) function that takes a &Vec<i32> and returns the same numbers converted to floating point as a &Vec<f64>? No, because where would the returned vector live (be owned)? In order to perform the conversion, you have to allocate a new vector, convert the numbers, and return the new vector by value. You can't just cast the &Vec<i32> to a &Vec<f64> and call it a day – they are different types with incompatible ABIs.

In the case of Box<dyn Trait>, there is of course no need for a reallocation. However, the vtable pointer still has to be created and added at some point. Therefore, simply treating (effectively transmuting) a &Box<T: Sized> as if it were a &Box<dyn Trait: !Sized> is incorrect – the two types don't have the same layout, contents, purpose, or usage (as far as the compiler is concerned w.r.t. accessing fields and generating code).

3 Likes

Thanks for going in depth. A large part of my journey with rust has been aligning my intuitions with how rust actually works under the hood. Those analogies really helped.

Thanks @trentj, that helped.

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.