Why does `Rc<RefCell<dyn Trait>>` work?

In my mental model of Rust, the following should not compile:

use std::cell::RefCell;
use std::rc::Rc;

fn main() {
    let x = Rc::new(RefCell::new(5));
    let a: Rc<RefCell<dyn std::fmt::Debug>> = x.clone();
    let b: Rc<RefCell<dyn std::fmt::Display>> = x.clone();
    
    println!("{:?} {}", a.borrow(), b.borrow());
}

(Playground)

But it does. In my mental model, an Rc<T> is a pointer to a tuple (ReferenceCount, T) on the heap. And RefCell<X> and RefCell<dyn T> are two different types, so the corresponding Rcs should be incompatible types, such that cloning one into the other should not work.

However, it does seem to work in the example. So where is the dyn hidden in Rc<RefCell<dyn Trait>>? It must be in the pointer inside Rc, which must become a fat pointer somehow. At least I don't see any other option.

Is this true? And if yes, is this somehow explainable with some general rules of how rust works, and can be applied to other (non-std-lib) examples? Or is this some special case built into the compiler just for Rc?

2 Likes

It works because the compiler applies an unsizing coercion.

This is a general concept you can apply to any type, though note that the pointer type (Rc in this case) must implement CoerceUnsized, which is an unstable trait.

4 Likes

Yes indeed! The vtable pointer is still part of the Rc.

Unsized types are accepted as fields in other struct, as long as it’s the last field; though this a feature you don’t necessarily come across all that often. It even works for your own structs.

use std::rc::Rc;
struct MyStruct<T: ?Sized> {
    counter: usize,
    final_field: T,
}

fn main() {
    let x = Rc::new(MyStruct {
        counter: 42,
        final_field: String::from("Hello"),
    });
    let a: Rc<MyStruct<dyn std::fmt::Debug>> = x.clone();
    let b: Rc<MyStruct<dyn std::fmt::Display>> = x.clone();

    println!("{:?} {}", &a.final_field, &b.final_field);
}

The coercion to an unsized type always happens once you have it behind some pointer indirection. The additional information (vtable, or slice length) becomes part of that pointer – which becomes fat – even when the “original” unsized type is nested within one or multiple layers of struct.

There’s another direction for incorporating additional layers of types into unsizing coercions; some types that wrap around the pointer support unsizing, too, though that’s currently not possible for user types, only for certain standard library ones, controlled via the CoerceUnsized trait. But this enables coercing Pin<Box<MyFutureType>> into Pin<Box<dyn Future<Output = Foo> + Send>>, for example.

7 Likes

Thanks for the pointers! Interesting that a type definition can "leak" into surrounding types like this. Meaning that the size of Box on the stack is not actually always just one pointer. Thanks for the clarifications!

Certainly, it’s interesting. If the Box’s size is surprising though… one way to think of this however is by focusing on the fact that MyStruct<dyn Display> itself is also an unsized type, just like dyn Display, so the fact that a Box<MyStruct<dyn Display>> is a fat pointer is not super surprising, once you’ve accepted that Box<dyn Display> is a fat pointer. Or maybe this was just an exercise in reiterating that for Box<dyn Display>, too, the vtable pointer is also clearly a part of (and makes the stack size bigger for) the Box itself, not it’s contents.

The things that “leak”, so to speak, is that MyStruct<dyn Display> just plain “inherits” that fat pointer metadata from the dyn Display inside, and that the type coercion in question, from MyStruct<String> to MyStruct<dyn Display> for instance, is allowed in the first place.

Yeah I think the surprising thing for me is that Box can be two pointers big if it is parameterised with a dyn Trait type. I mean, it is not totally surprising, because when reading about dyn Trait this is usually explained well. But coming from the other end like I did in this thread was more surprising. Nice to put these concepts together though.

The corresponding section in @quinedot's tour of dyn Trait. @isibboi I can highly recommend reading the entire transcript from him - it's really awesome. :slight_smile:

4 Likes

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.