Why does `dyn Trait` require a Box?

It seems to me that Box<dyn MyTrait> construction just repeats itself without bringing any benefits.

There is a symmetry between Box<dyn MyTrait> and Rc<dyn MyTrait>, but enabling clearer code like Vec<dyn MyTrait> instead of Vec<Box<dyn MyTrait>> seems to be more important than preserving symmetry.

Are there other reasons for not enabling bare dyn Trait used as a pointer type that I don't see?

2 Likes

And &dyn MyTrait, and &mut dyn MyTrait, and &RefCell<dyn MyTrait>, and Arc<Mutex<dyn MyTrait>>, and struct MyType<T: ?Sized> { ... }... There are a lot of ways you can use unsized types besides just Box and Rc.

If dyn Trait was a pointer, for one thing we wouldn't be able to express ownership and sharing of trait objects using the same generic types that we use for everything else. Look through the standard library for all the places you see a T: ?Sized bound, and imagine making those work without bare trait objects. Take Mutex for example -- how would you represent what today is Arc<Mutex<dyn Trait>> if you couldn't name the type dyn Trait? Mutex<dyn Trait> wouldn't work because that would be a pointer in a mutex, so you'd have to create a special pointer type or something...

For that matter, what would be the ownership semantics of dyn Trait? Would dyn Trait be the same as Box<dyn Trait> today, and invent a new syntax for Rc<dyn Trait>? Would you create new syntax for every possible pointer type?? I can't even begin to imagine how that would work.

dyn Trait is a relatively simple language feature that composes well with all the other stuff in the language and the standard library. It's easy to write a struct or a function that is generic, and with the simple addition of a ?Sized bound make it work with trait objects too. It's a little quirky, I suppose, but that's the cost of dynamic dispatch in a language that is otherwise very rigidly typed.

I didn't understand this part. What's the problem with a pointer in a Mutex? Same with Rc<dyn Trait>: read it as the present Rc<Box<dyn Trait>>. It seems that Rust used to require wrapping a type into a Box before Rc, and generally many nested pointer types seem to be very common, so the compiler should be ready to optimize such things and not require double indirection, or am I wrong? So if dyn Trait is a pointer, and everything that uses it has another pointer indirection level inside it, there should be no runtime performance penalty.

dyn Trait is not a pointer. It is a value of a dynamically determined size. You can generally only interact with them behind a pointer type like Box<dyn Trait>, Rc<dyn Trait>, &dyn Trait, etc.

11 Likes

Arc<Mutex<Box<dyn Trait>>> is not semantically equivalent to Arc<Mutex<dyn Trait>>. It's not possible to merely "optimize away" the double indirection because in the first case, you have to lock the mutex before you can read the inner pointer -- whether or not you dereference it.

As an example (by no means the only example) of why the double indirection is necessary, consider this function:

fn set(ptr: &Mutex<Box<dyn Trait>>, value: impl Trait) {
    *ptr.lock().unwrap() = Box::new(value);
}

You can't do this with &Mutex<dyn Trait> because it has only one indirection; the Mutex has to have a fat pointer inside it. But having only one indirection might be something you want, sometimes, and if you couldn't express &Mutex<dyn Trait>, you'd always have to pay the double indirection cost, just because it's possible to write the set function above.

1 Like

Each trait object may have a different size, but all elements in Vec must have the same size. If one dyn Trait was 1 byte, another was 100 bytes, and another was 5 bytes, you couldn't address any trait object's data with just vec[n] in constant time, because there's no rule what data starts where — after all each size is dynamic known only at runtime, so the Vec would need to scan and measure all n-1 elements to know where the nth one starts.

OTOH every Box<dyn Trait> takes 2 usize, so vec[n] can use easy multiplication by a constant n*2*sizeof(usize) to get a pointer to each traits' data.

Strictly speaking, it doesn't have to be Box. All other forms of indirection are fine too, e.g. &mut dyn Trait, Arc<dyn Trait>, etc., because they too have a fixed pointer-like size.

5 Likes

@trentj let me rephrase this way - aren't Box<dyn Trait>, NonNull<dyn Trait>, Unique<dyn Trait> and *dyn Trait all basically usize pointers (in machine, not Rust sense) + vtable pointer, attached different semantics? And in Mutex<dyn Trait>, apart from Mutex's atomic lock word and flag fields, isn't the "dyn Trait" part is a usize machine pointer + vtable pointer as well? If so, in your example:

*ptr.lock().unwrap() = Box::new(value);

We could understand, in theory, that we assign one usize machine pointer + vtable pointer to another machine pointer + vtable pointer, and if we could prove that this conversion is safe semantically, do it. Then if somebody wants to reference that Box from inside Mutex, we "recreate" it each time (but in fact, again, just making semantic conversion).

Mutex<T> stores the value inline, it’s not heap allocated; the value is kept inside an UnsafeCell. This means you can’t have a Mutex<dyn Trait> on its own. You can unsize it only if you have it behind a pointer already, such as Arc<Mutex<...>>.

@leventov, cool to see you showing up in the Rust world (had enough of Java? :slight_smile:)

4 Likes

Thank you.

I explored some structure sizes (playground):

usize: 8
Box<dyn Trait>: 16
Arc<prim>: 8
Arc<dyn Trait>: 16
Box<Mutex<prim>>: 8
Box<Mutex<dyn Trait>>: 16

Apparently the vtable pointer transcends the Mutex<...> wrapping, making Mutex<dyn Trait> a trait object itself?

--

@vitalyd I find myself writing so much Unsafe code, with essentially only guarantees of C and syntax even inferior to C, that it doesn't make much sense.

Mutex<T> can be a dynamically sized type (DST) because it allows T: ?Sized (because UnsafeCell<T> allows it). Basically, Mutex<T> has some static size and the last field possibly dynamic (that’s where the UnsafeCell is). Most uses involve a sized type. To be able to actually use unsized values, you need to put the Mutex behind some form of pointer, such as Box, Arc, etc.

Taking Box<Mutex<i32>> as an example. This is a Box that has a thin pointer (and isn’t a trait object). Given Mutex allows DST and unsizing, we can unsize this to, eg, Box<Mutex<dyn Debug>>:

let b = Box::new(Mutex::new(5)) as Box<Mutex<dyn Debug>>;

It doesn’t make the mutex itself a trait object - the trait object is what’s inside the mutex. Or more accurately, the Box has a fat ptr now.

1 Like

Oh as for ...

... yeah, I hear you - it’s an unpleasant experience.

I mean that Box<Mutex<dyn Debug>> could apparently be transmuted into a std::raw::TraitObject. Box has type parameter T, how does it (or more likely something that backs it up, probably *T) determine whether it should be a thin or a fat pointer? I imagine that there should be some check a-la is_trait_object<T>() inside the compiler, that should return true for Mutex<dyn Debug>.

Unsizing is handled by the compiler at specific coercion sites - I think the original DST coercion RFC is still relevant.

1 Like