2

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

pretzelhammer
  • 13,874
  • 15
  • 47
  • 98
CoronA
  • 7,717
  • 2
  • 26
  • 53

2 Answers2

6

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.

pretzelhammer
  • 13,874
  • 15
  • 47
  • 98
  • Maybe my understanding of `'static` is wrong (I added my understanding to the upper post). If this is so, can you explain to me why nodes that get added (or removed) later on can have a `'static` lifetime? – CoronA May 10 '20 at 14:23
  • @CoronA I've updated my answer above, please let me know if it helps, thanks. – pretzelhammer May 10 '20 at 14:39
  • Maybe it is a step in the right direction. But I do not understand why the lifetime is a problem: The argument `Arc>` ensures that `T` lives at least as long as Arc, so casting to `Arc>>` should infer this lifetime for the trait object (for every lifetime). Is this idea wrong or is the compiler not as clever? – CoronA May 10 '20 at 15:10
  • @CoronA if you remove the `as NodeRef` cast you will still get the same compiler error about `T` not living long enough. The issue has nothing to do with casting an `Arc>` to a `Arc>>`. [playground](https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=d13ec50f03b456c530645b3f1a8ad632) – pretzelhammer May 10 '20 at 15:17
  • There is an implicit cast in `edge.connect(self_clone);`. Replace this line with `self.clone().connect(edge_ref.clone());`, it compiles (it is endless recursion, so only build it). For me, is seems that casting to the trait object is the problem. [playground](https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=3e69dfbccfa4965b0ff2c69a002e9131) – CoronA May 10 '20 at 15:33
  • @CoronA I've updated my answer again, please let me know if it helps, thanks. – pretzelhammer May 10 '20 at 16:31
  • That answers my question. The link an trait object default lifetimes was very helpful. I am just curious where the meaning of `'static` (as constraint of generic type parameters) is documented officially. Although your description proved right, I did not find another resource confirming it explicitly. – CoronA May 11 '20 at 04:53
  • I found a confirming explanation [here](https://stackoverflow.com/questions/40053550/the-compiler-suggests-i-add-a-static-lifetime-because-the-parameter-type-may-no). – CoronA May 11 '20 at 05:16
2

The type Arc<RwLock<T>> could potentially be a reference. Since it is a generic and T is not yet defined. When you attempt to use it with dyn then T becomes a reference, though not quite the same as a normal reference.

Rust By Example has a simple explanation here

To solve this you could change T: Node, to T: Node + 'static as the compiler may suggest or you could wrap your dyn Node in a RefCell.

Sean
  • 123
  • 4
  • From my point of view the `T` could not be `'static` when such nodes could be added interactively to the graph (maybe my point of view is wrong). About the hint with `RefCell` - is there a difference to `RwLock`? – CoronA May 10 '20 at 14:31
  • RwLock allows you to explicitly specify when a thread is using data. All checks are made during compilation. RefCell does something similar, but does it at runtime. Though it _does_ means you have a little more cpu overhead while running. Check [this](https://doc.rust-lang.org/book/ch15-05-interior-mutability.html) out for a lot more info. – Sean May 11 '20 at 01:30
  • My question was not for the general difference - I asked whether this would change anything considering the lifetimes. Probably not. On `RwLock` and `RefCell`: Look [here](https://ricardomartins.cc/2016/06/25/interior-mutability-thread-safety), I think both do runtime checks. – CoronA May 11 '20 at 04:44