Casting Rc<ConcreteType> to an Rc<Trait>

  • A+

Horse is a struct which implements the Animal trait. I have an Rc<Horse> and a function that needs to take in an Rc<Animal>, so I want to convert from Rc<Horse> to Rc<Animal>.

I did this:

use std::rc::Rc;  struct Horse;  trait Animal {}  impl Animal for Horse {}  fn main() {     let horse = Rc::new(Horse);     let animal = unsafe {         // Consume the Rc<Horse>         let ptr = Rc::into_raw(horse);         // Now it's an Rc<Animal> pointing to the same data!         Rc::<Animal>::from_raw(ptr)     }; } 

Is this a good solution? Is it correct?


The answer by Boiethios already explains that upcasting can be explicitly performed using as, or even happens implicitly in certain situaions. I'd like to add a few more detail on the mechanisms.

I'll start with explaining why your unsafe code works correctly.

let animal = unsafe {     let ptr = Rc::into_raw(horse);     Rc::<Animal>::from_raw(ptr) }; 

The first line in the unsafe block consumes horse and returns a *const Horse, which is a pointer to a concrete type. The pointer is exactly what you'd expect it to be – the memory address of horse's data (ignoring the fact that in your example Horse is zero-sized and has no data). In the second line, we call Rc::from_raw(); let's look at the protoype of that function:

pub unsafe fn from_raw(ptr: *const T) -> Rc<T> 

Since we are calling this function for Rc::<Animal>, the expected argument type is *const Animal. Yet the ptr we have has type *const Horse, so why does the compiler accept the code? The answer is that the compiler performs an unsized coercion, a type of implicit cast that is performed in certain places for certain types. Specifically, we convert a pointer to a concrete type to a pointer to any type implementing the Animal trait. Since we don't know the exact type, now the pointer isn't a mere memory address anymore – it's a memory address together with an identifier of the actual type of the object, a so-called fat pointer. This way, the Rc created from the fat pointer can retain the information of the underlying concrete type, and can call the correct methods for Horse's implementation of Animal (if there are any; in your example Animal doesn't have any functions, but of course this should continue to work if there are).

We can see the difference between the two kinds of pointer by printing their size

let ptr = Rc::into_raw(horse); println!("{}", std::mem::size_of_val(&ptr)); let ptr: *const Animal = ptr; println!("{}", std::mem::size_of_val(&ptr)); 

This code first makes ptr a *const Horse, prints the size of the pointer, then uses an unsized coercion to convert ptr to and *const Animal and prints its size again. On a 64-bit system, this will print

8 16 

The first one is just a simple memory address, while the second one is a memory address together with information on the concrete type of the pointee. (Specifically, the fat pointer contains a pointer to the virtual method table.)

Now let's look at what happens in the code in Boethios' answer

let animal = horse as Rc<Animal>; 

or equivalently

let animal: Rc<Animal> = horse; 

also perform an unsized coercion. How does the compiler know how to do this for a Rc rather than a raw pointer? The answer is that the trait CoerceUnsized exists specifically for this purpose. You can read the RFC on coercions for dynamically sized types for further details.


:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen: