Search code examples
genericsrustclonetraitstrait-objects

Cast Arc<RwLock<T>> to Arc<RwLock<TraitObject>


I am writing a graph implementation with edges and nodes. The graph should be accessed concurrently so I chose to build the Edges and Nodes as Arc<Mutex<dyn Edge>> and Arc<RwLock<dyn Node>>.

Unfortunately I get a compile error the parameter type 'T' may not live long enough (Playground) when connecting nodes/edges.

pub trait Node {
  fn connect(&mut self, edge: EdgeRef);
}

pub type NodeRef = Arc<RwLock<dyn Node>>;

pub trait Edge {
  fn connect(&mut self, node: NodeRef);
}

pub type EdgeRef = Arc<Mutex<dyn Edge>>;

impl<T> Node for Arc<RwLock<T>>
where
  T: Node,
{
  fn connect(&mut self, edge_ref: EdgeRef) {
    let mut node = self.write().unwrap();
    let mut edge = edge_ref.lock().unwrap();
    let self_clone = self.clone() as NodeRef; // the parameter type `T` may not live long enough
    edge.connect(self_clone);
    node.connect(edge_ref.clone());
  }
}

The problem is: An Arc<RwLock<T>> should is not a reference so there should be no lifetime. Casting it to Arc<RwLock<dyn Node>> also does not introduce lifetimes.

Can someone explain this compiler error? Is this problem related to every parametric type (e.g. Type<T>) or only to Arc<RwLock<T>>?


Solution

  • The compile error explains how to fix the problem:

    error[E0310]: the parameter type `T` may not live long enough
      --> src/lib.rs:22:22
       |
    15 | impl<T> Node for Arc<RwLock<T>>
       |      - help: consider adding an explicit lifetime bound...: `T: 'static`
    ...
    22 |     let self_clone = self.clone() as NodeRef;
       |                      ^^^^^^^^^^^^
       |
    note: ...so that the type `T` will meet its required lifetime bounds
      --> src/lib.rs:22:22
       |
    22 |     let self_clone = self.clone() as NodeRef;
       |                      ^^^^^^^^^^^^
    
    error: aborting due to previous error
    
    For more information about this error, try `rustc --explain E0310`.
    

    Adding + 'static to your T's bounds does indeed fix the error:

    use std::sync::{Arc, Mutex, RwLock};
    
    pub trait Node {
      fn connect(&mut self, edge: EdgeRef);
    }
    
    pub type NodeRef = Arc<RwLock<dyn Node>>;
    
    pub trait Edge {
      fn connect(&mut self, node: NodeRef);
    }
    
    pub type EdgeRef = Arc<Mutex<dyn Edge>>;
    
    impl<T> Node for Arc<RwLock<T>>
    where
      T: Node + 'static, // added "+ 'static" here
    {
      fn connect(&mut self, edge_ref: EdgeRef) {
        let mut node = self.write().unwrap();
        let mut edge = edge_ref.lock().unwrap();
        let self_clone = self.clone() as NodeRef;
        edge.connect(self_clone);
        node.connect(edge_ref.clone());
      }
    }
    

    playground

    But why do I need a lifetime bound when my T will never be a reference? you ask. Well, the Rust compiler doesn't know that yet, a T can be any type, including references. The set of types represented by T includes the set of types represented by &T and &mut T. Both &T and &mut T are subsets of T. That's why you have to put a lifetime bound on T, it's your way of communicating to the compiler that your T will only be owned types or static references.

    More on 'static lifetimes

    'static is a misleading name for the lifetime because it causes most people to think 'static types have to live for the entire duration of the program and cannot be dynamically allocated or dropped. Neither of these are true in reality: 'static types can be dynamically allocated and they also can be dropped. What 'static really means in practice is "you can safely hold on to this type indefinitely". All "owned types" like String and Vec are 'static. Here's a Rust program which I hope illustrates this point:

    use rand::prelude::*; // 0.7.3
    
    // this function takes 'static types and drops them
    // no compiler errors because 'static types can be dynamically allocated and dropped
    fn is_static<T: 'static>(t: T) {
        std::mem::drop(t)
    }
    
    fn main() {
        let string = String::from("string"); // dynamically allocated string
        is_static(string); // compiles just fine
    
        let mut strings: Vec<String> = Vec::new();
        let mut loops = 10;
        while loops > 0 {
            if rand::random() {
                strings.push(format!("randomly dynamically allocated string on loop {}", loops));
            }
            loops -= 1;
        }
    
        // all the strings are 'static
        for string in strings {
            is_static(string); // compiles no problem
        }
    }
    

    playground

    More on lifetime elision and default trait object lifetimes

    You define NodeRef and EdgeRef as such:

    pub type NodeRef = Arc<RwLock<dyn Node>>;
    pub type EdgeRef = Arc<Mutex<dyn Edge>>;
    

    However the Rust compiler interprets those like so:

    pub type NodeRef = Arc<RwLock<dyn Node + 'static>>;
    pub type EdgeRef = Arc<Mutex<dyn Edge + 'static>>;
    

    So when you want to cast some Arc<RwLock<T>> to NodeRef then T must be bounded by Node + 'static because NodeRef also has those bounds, i.e. Arc<RwLock<dyn Node + 'static>>. All trait objects in Rust have lifetimes, but you don't usually write them since Rust infers them for you. The Rust Reference has a thorough explanation on lifetime elision and default trait object lifetimes if you'd like to learn more.

    You can ease the 'static requirement by making your type aliases generic over 'a:

    pub type NodeRef<'a> = Arc<RwLock<dyn Node + 'a>>;
    pub type EdgeRef<'a> = Arc<Mutex<dyn Edge + 'a>>;
    

    However that will dramatically increase the complexity of your code and I'm pretty sure you want to stick with 'static as it already supports what you're trying to do.