What are the rules for lifetimes in Rust when using boxed trait objects?

Viewed 53

The code below doesn't compile with error E0597: borrowed value (two) doesn't live long enough.

fn main() {
    let one = String::from("one");

    let mut _it: Box<dyn Iterator<Item=char>> = Box::new(one.chars());

    let two = String::from("two");

    _it = Box::new(two.chars());
}

But, if instead of boxed trait object I use reference to a trait object it works:

fn main() {
    let one = String::from("one");

    let mut _it: &dyn Iterator<Item=char> = &one.chars();

    let two = String::from("two");

    _it = &two.chars();
}

Also, if I don't use trait object it works too:

fn main() {
    let one = String::from("one");

    let mut _it: Box<_> = Box::new(one.chars());

    let two = String::from("two");

    _it = Box::new(two.chars());
}

Why?

1 Answers

Let's take them one by one.

In your first example, the problem is subtle and hinted at by the error message

8 |     _it = Box::new(two.chars());
  |                    ^^^^^^^^^^^ borrowed value does not live long enough
9 | }
  | -
  | |
  | `two` dropped here while still borrowed
  | borrow might be used here, when `_it` is dropped and runs the destructor for type `Box<dyn Iterator<Item = char>>`
  |
  = note: values in a scope are dropped in the opposite order they are defined

As the note says, values are dropped in the opposite order they are defined. As two is defined in line 6 and _it is defined (not assigned) in line 4, the compiler will try to destroy two before it tries to destroy _it at the end of the function. But by the time we reach the end of the function, _it holds a reference to two (line 8), and the imposed ordering would destroy two while _it still holds a reference to it. The destructor of _it could observe the destroyed value two, so this is not allowed.

Why is there even a destructor? Because the trait object dyn Iterator... - just like any other trait object - could hold some dynamic type that has a destructor. And that destructor might observe things it potentially references; as far as the compiler knows, that's two. While the actual Chars-type does not suffer from this problem, I could conjure up such a type and stick it into a dyn Iterator.... So with trait objects, the order in which values are dropped is always important.

The solution is to change the order of definition:

fn main() {
    let one = String::from("one");

    let two; // Notice the definition, before `_it` so it gets dropped *after*

    let mut _it: Box<dyn Iterator<Item=char>> = Box::new(one.chars());

    two = String::from("two");

    _it = Box::new(two.chars());
}

In the second example, you are using plain references. Since plain references do not have destructors, there is no destructor that could potentially observe a destroyed value while it is running, and the order in which values are destroyed is not important; so it compiles just fine.

In the third example, there are no trait objects involved, the boxed type is simply a normal type as far as the compiler is concerned (just like Box<u32>). Because it is a concrete type, the compiler can figure out that the type inside the Box (which is a Chars, and holds a reference of some lifetime), has a trivial destructor (which does nothing), and therefore the Box has a trivial destructor that simply deallocates; so it doesn't matter that two is in fact destroyed before _it, because _it will definitely not be able to observe two in its destructor.

Related