Making a type-erasing function idempotent

โš“ Rust    ๐Ÿ“… 2025-08-22    ๐Ÿ‘ค surdeus    ๐Ÿ‘๏ธ 5      

surdeus

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:

  1. Include a special method in SomeTrait:

    trait SomeTrait: core::fmt::Debug {
        fn into_boxed(self) -> Box<dyn SomeTrait>
        where
            Self: Sized + 'static,
        {
            Box::new(self)
        }
    }
    
    impl SomeTrait for Box<dyn SomeTrait> {
        fn into_boxed(self) -> Box<dyn SomeTrait> {
            self
        }
    }
    

    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.

  2. 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.

  3. 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?

5 posts - 3 participants

Read full topic

๐Ÿท๏ธ Rust_feed