4ntoine
4ntoine

Reputation: 20408

How to filter RCed trait objects vector of specific subtrait in Rust?

The task is to filter out the supertrait objects for base trait object vector:

use std::rc::Rc;
use std::any::Any;

pub trait TraitA {
    fn get_title(&self) -> &str;
    fn as_any(&self) -> Any;
}

pub trait TraitB: TraitA {
    fn get_something(&self) -> &str;
}

pub fn filter_b(input: Vec<Rc<dyn TraitA>>) -> Vec<Rc<dyn TraitB>> {
    // bs.filter(|it| /* How to do it? */).collect();
}

Is it even possible? Any clue or advice?

I know as_any() can be used to downcast but as i'm not sure how it's meant to work with Rc as it takes ownership (and thus requires the instance).

Upvotes: 2

Views: 338

Answers (1)

kmdreko
kmdreko

Reputation: 60022

I was first expecting the answer to be "absolutely not!", Any doesn't help if you don't know the concrete type. But it turns out you can... with caveats, and I'm not 100% sure its totally safe.

To go from Rc<T> to Rc<U>, you can use the escape hatches into_raw and from_raw. The docs of the former read:

Constructs an Rc from a raw pointer.

The raw pointer must have been previously returned by a call to Rc<U>::into_raw where U must have the same size and alignment as T. This is trivially true if U is T. Note that if U is not T but has the same size and alignment, this is basically like transmuting references of different types. See mem::transmute for more information on what restrictions apply in this case.

The user of from_raw has to make sure a specific value of T is only dropped once.

This function is unsafe because improper use may lead to memory unsafety, even if the returned Rc<T> is never accessed.

With that in mind, since we only have access to TraitA, it'll need an as_b() function to get itself as a TraitB. The fact that the target is a super trait doesn't really help. Then we can write a crosscast function like so:

use std::rc::Rc;

trait TraitA {
    fn print_a(&self);

    // SAFETY: the resulting `dyn TraitB` must have the *exact* same address,
    // size, alignment, and drop implementation for `crosscast` to work safely.
    // Basically it must be `self` or maybe a transparently wrapped object.
    unsafe fn as_b(&self) -> Option<&(dyn TraitB + 'static)>;
}

trait TraitB {
    fn print_b(&self);
}

fn crosscast(a: Rc<dyn TraitA>) -> Option<Rc<dyn TraitB>> {
    unsafe {
        let b_ptr = a.as_b()? as *const dyn TraitB;
        let a_ptr = Rc::into_raw(a);
        
        // sanity check
        assert!(a_ptr as *const () == b_ptr as *const ());
        
        Some(Rc::from_raw(b_ptr))
    }
}

With this function at our disposal, your problem becomes trivial by using .filter_map():

struct Both {
    data: String,
}

impl TraitA for Both {
    fn print_a(&self) { println!("A: {}", self.data); }
    unsafe fn as_b(&self) -> Option<&(dyn TraitB + 'static)> { Some(self) }
}

impl TraitB for Both {
    fn print_b(&self) { println!("B: {}", self.data); }
}

struct OnlyA {
    data: String,
}

impl TraitA for OnlyA {
    fn print_a(&self) { println!("A: {}", self.data); }
    unsafe fn as_b(&self) -> Option<&(dyn TraitB + 'static)> { None }
}

fn main() {
    let vec_a = vec![
        Rc::new(Both{ data: "both".to_owned() }) as Rc<dyn TraitA>,
        Rc::new(OnlyA{ data: "only a".to_owned() })
    ];

    for a in &vec_a {
        a.print_a();
    }
    
    println!();
    
    let vec_b = vec_a
        .into_iter()
        .filter_map(crosscast)
        .collect::<Vec<_>>();

    for b in &vec_b {
        b.print_b();
    }
}

See it all together on the playground.

I would still recommend not doing this if at all possible. It would be perfectly safe for example to go from &Rc<dyn TraitA> to Option<&dyn TraitB> using the above method without all the restrictions. Something like this wouldn't have the restrictions and unsafety:

for b in vec_a.iter().filter_map(|a| a.as_b()) {
   // ...
}

Upvotes: 2

Related Questions