Consider the following program which is performing type erasure involving boxing:
trait SomeTrait: core::fmt::Debug {}
impl SomeTrait for u32 {}
impl SomeTrait for Box<dyn SomeTrait> {}
fn into_boxed<T: SomeTrait + 'static>(value: T) -> Box<dyn SomeTrait> {
Box::new(value)
}
#[test]
fn not_double_boxing() {
let b = into_boxed(123);
let a1 = &raw const *b;
let b = into_boxed(b);
let a2 = &raw const *b;
println!("{b:?}");
assert_eq!(a1, a2);
}
The test fails, of course, because each time into_boxed() is called, it adds a new Box. But suppose I want to avoid double boxing, and have into_boxed() return its input if that input is Box<dyn SomeTrait>. I know of three ways to do this, all of which will make the above test pass:
This is straightforward, using provided methods to give a somewhat specialization-like effect. However, it requires that SomeTrait know about all the kinds of type erasure we want ā for example, if you want Box<dyn SomeTrait + Send>, the trait has to include a method for that too.
Compare type IDs and transmute.
fn into_boxed<T: SomeTrait + 'static>(value: T) -> Box<dyn SomeTrait> {
if TypeId::of::<T>() == TypeId::of::<Box<dyn SomeTrait>>() {
// SAFETY:
// * we just checked that the type is equal
// * the duplication caused by `transmute_copy` is canceled out by `ManuallyDrop`
unsafe { core::mem::transmute_copy(&ManuallyDrop::new(value)) }
} else {
Box::new(value)
}
}
This requires no cooperation from the trait, but is unsafe and requires T: 'static.
Compare type IDs and downcast. This is tricky, since dyn Any is unsized and the purpose is avoiding heap allocation, but can be done using a combination of &mut and Option:
fn into_boxed<T: SomeTrait + 'static>(value: T) -> Box<dyn SomeTrait> {
let value_in_option = &mut Some(value);
if let Some(value_of_same_type) = <dyn Any>::downcast_mut::<Option<Box<dyn SomeTrait>>>(value_in_option) {
value_of_same_type.take().unwrap()
} else {
Box::new(value_in_option.take().unwrap())
}
}
This has the same effect and performance as the transmute, and is safe, but inelegant to read.
My questions about all this are:
Is there a more flexible way? In particular, one which, unlike option 1, does not require cooperation from SomeTrait, and unlike option 3, does not require T: 'static? I suspect not, but perhaps thereās some angle on it I havenāt thought of. (It would be fine to use a trait separate from SomeTrait, but I donāt think thereās a way to achieve that short of separately ā not generically ā implementing it for every SomeTrait implementor.)
Is there a way to write option 3 with less clutter, that is still safe?
they all are interesting. I prefer option 2.
Option 2:
Because: I familiar with Into and From traits,⦠in rust
But We have more options 3 and 4. Iām wondering what advantages they have. I need more examples about type comparison and downcast . Itās quite interesting
I am not asking for preferences among three options. I am asking for solutions to the stated problem that are technically superior to any of the three I have so far.
until proper speiclalization (or, maybe "overloading") is supported, I don't see a way other than to branch on the type id. currently we just don't have the ability have two implementations of the same function and communicate to the compiler to pick one based on some resolution priority rules (analog to C++'s overloading).
not to my knowlege, at least not on stable.
on nightly, as a variant to your method #2, I think maybe you can compare the vtable pointers instead of TypeId, so it does not require the type to be 'static?
EDIT: never mind, you can't, see comment by @quinedot below.
but the code is even more hideous to read, and still needs the unsafe transmute_copy() anyway, so I don't see it as an improvement at all (other than it relaxes the 'static requirement), something like:
However, note that comparing trait object pointers (*const dyn Trait) is unreliable: pointers to values of the same underlying type can compare inequal (because vtables are duplicated in multiple codegen units), and pointers to values of different underlying type can compare equal (since identical vtables can be deduplicated within a codegen unit).
In my actual application, not double-boxing is strictly an optimization. Therefore, an āunreliableā option is actually acceptable, if it happens reliably enough in the cases that matter, and an identical vtable pointer implies identical future behavior even if the type isn't actually equal.
But, comparing vtable pointers in my actual use case could enable lifetime transmutation. It didnāt occur to me that it was significant that my actual trait has a type parameter, and that type parameter can contain lifetimes, allowing Box<dyn SomeTrait<&'a str>> and Box<dyn SomeTrait<&'b str>> to exist with the same vtable pointer. I suspect that it would be sound anyway, because the normal boxing would fail with a lifetime error in all the cases the transmute would be unsound, but thatās a weaker chain of logic than I care to risk for this purpose.
It's not necessarily sound. Here's a POC of how the invariance of the trait parameter can be relied upon for soundness for an implementor that is itself 'static, despite storing and sharing non-'static things.[1]