Upcasting trait in an option field

79 Views Asked by At

I am just learning Rust and have some issues with upcasting a trait ref inside an option. I want to pass it as ref to reduce the overhead.

The basic idea is as follows: I have some objects (children) defined somewhere else in the code, they are of type ChildT. I need the master to be able to access their properties and manipulate them. There are objects, that also need to access the children - either they have direct access or via master.get_child. When coming from C++, I would like to store a pointer in the struct. It would avoid copy overhead and the ref would stay the same along all objects that want to access the respective child

When implementing mutability, it gets even more complicated, I tried to overcome it this way:

#![allow(unused)]

use num_traits::Num;
use std::{any::Any, rc::Rc};

pub trait AsBase{

    fn as_base(&self) -> &dyn BaseT;
}

impl <T: BaseT + Sized> AsBase for T{
    fn as_base(&self) -> &dyn BaseT{
        self
    }

}

trait BaseT: AsBase{
    fn get_child(&self) -> Option<Rc<&dyn BaseT>>;
    fn get_child_mut(&mut self) -> Option<&mut Rc<&dyn BaseT>>;
}

trait ChildT: BaseT{
    fn child_t_fn(&self);
}

struct C1{
    child: Option<Rc<dyn ChildT>>,
}

impl BaseT for C1{
    fn get_child(&self) -> Option<Rc<&dyn BaseT>> {
        match &self.child{
            Some(x) => return Some(Rc::new(x.as_base()) ),
            None => return None
        }
    }
    fn get_child_mut(&mut self) -> Option<&mut Rc<&dyn BaseT>> {
        match &self.child{
            Some(x) => return Some(&mut Rc::new(x.as_base()) ),
            None => return None
        }
    }
}

impl ChildT for C1{
    fn child_t_fn(&self) {
        
    }
}

// same as C2 code-wise, for the sake of simplicity
struct C2{
    child: Option<Rc<dyn ChildT>>,
}

impl BaseT for C2{
    fn get_child(&self) -> Option<Rc<&dyn BaseT>> {
        match &self.child{
            Some(x) => return Some(Rc::new(x.as_base()) ),
            None => return None
        }
    }
    fn get_child_mut(&mut self) -> Option<&mut Rc<&dyn BaseT>> {
        match &self.child{
            Some(x) => return Some(&mut Rc::new(x.as_base()) ), // <- cannot pass temp. variable
            None => return None
        }
    }
}
impl ChildT for C2{
    fn child_t_fn(&self) {
        
    }
}

// receive two structs, change one child
fn test(c1: &mut Rc<&dyn ChildT>, c2: &mut Rc<&dyn ChildT>){
    let old_child = c2.get_child_mut();

    let mut new_val: Rc<&dyn BaseT> = Rc::new(c1.as_base());
    old_child = Some(new_val); // <- doesnt work. cannot make it mutable (I think because it is temp and would pass to another struct losing scope. how to overcome that?)
}

fn main(){
    // Example use of the upper code
    let c1 = C1{
            child: None
        };
    let mut c2 = C2{
        child: Some(Rc::new(c1))
    };
    let mut c3 = C2{
        child: Some(Rc::new(c2))
    };
    let mut c4 = C1{
        child: Some(Rc::new(c3))
    };
    
    // i just skipped the function .as_child(), it is basically the same as to_base
    test(Rc::new(c4.as_child()),Rc::new(c2.as_child()));


}

1

There are 1 best solutions below

0
On

First off, a little disclaimer: Rust works quite different than other languages. Rust has the claim to have zero undefined behaviour, and that causes a number of compiler enforced Rules that simply do not exist in C++. So many programming paradigms that work in other languages will not be transferrable to Rust and need a rethinking of the architecture. Especially for people that are already quite formidable in other languages, this can be a challenge.

That said, there are usually really good alternatives which in the end even tend to make the code cleaner, but it might take a while to understand the problems and its potential solutions.

In your case there are several things that need rework:

  • an Rc<&> (reference counted reference) makes no sense, because you now get the drawbacks of both Rc and & without any added benefits. You now have an immutable reference counter that still has a lifetime attached (which is the main thing Rc tries to avoid), so in all cases, a & is superior to a Rc<&>. So I'll assume that all the functions that return a Rc<&> should actually return a &.
  • You talk about modifying things. One thing that emerged from Rusts zero-undefined-behavior rule is the borrow checker; it makes sure that there is either multiple references to an object or mutability, but never both. You cannot have multiple mutable references to the same object, as it would be super easy to create undefined behavior from that. For that reason, everything inside of an Rc is immutable - you need a runtime check to get mutability back. That can be achieved by RefCell, Cell, Mutex or Atomics, depending on the usecase. In your case it's an entire object, so RefCell or Mutex are the only valid options, and Mutex (thread safe) only makes sense when paired with an Arc instead of an Rc, so I assume that the correct choice would be an Rc<RefCell<>> in your case.
  • Now we have the problem that Rc<RefCell<>> no longer allows as to get a pure & reference to our object. Every access to the interior of a RefCell needs to be counted at runtime - that's why refcell's borrow function returns a token object that does the runtime reference counting, and that object needs to stay alive while you use the & reference. That means you can no longer return a & reference from your function.

So there are multiple ways to proceed:

  • Convert the Rc's in your code to Box. Box is the same as Rc but without the reference counter - it simply puts an object on the heap, which allows you to store dyn objects. The fact that it doesn't allow sharing now allows getting direct & references and even &mut references out of it; although your main (specifically c2) suggests that sharing is a necessity for you.
  • Convert the Rc's to Rc<RefCell<>>'s and deal with the consequences. That seems most likely in your case. The next problem we need to solve here is how to get the &mut dyn BaseT's you need in your test function to modify a node. Sadly, this isn't a trivial problem to solve.

So I sadly think the answer is: Does your code really need to do those things? Are there other ways that would work better? I am afraid a re-thinking of your architecture might be the best solution. I sadly cannot help you with that as of now, because it's really not clear what actual problem you are trying to solve with your code.