Lifetime help - specify that a reference is dropped correctly to the compiler

Hello, I'm new here, please be nice :slight_smile:

Suppose I have this struct, with these implemented functions:

use std::collections::BTreeMap;
use std::fmt::Debug as DebugTrait;
pub struct Font;
pub struct DebugRenderer<'a> {
    pub font: &'a Font; // not important for this example, must live for the lifetime of this struct
}
impl DebugRenderer<'_> {
    // a canvas is passed here, but I will use println! instead to demonstrate
    fn draw_to_canvas(&self, values: &BTreeMap<&'static str, &dyn DebugTrait>) {
        for item in values.iter() {
            println!("{0}: {1:?}", item.0, item.1);
        }
    }
}

However, with this main function, it doesn't compile: Playground

fn main() {
    let mut values: BTreeMap<&'static str, &dyn DebugTrait> = BTreeMap::new();
    let font = Font;
    let debug_renderer = DebugRenderer { font: &font };
    for i in 1..=5 {
        values.insert("example value", &i);
        debug_renderer.draw_to_canvas(&values);
        values.clear();
        println!("hey {i}");
    }
}

(creating a BTreeMap outside of a loop, passing in a reference in the loop, clearing the map [and therefore its stored references] before the next iteration [which should resolve lifetime issues?])

Here's the error:

   Compiling playground v0.0.1 (/playground)
error[E0597]: `i` does not live long enough
  --> src/main.rs:22:40
   |
21 |     for i in 1..=5 {
   |         - binding `i` declared here
22 |         values.insert("example value", &i);
   |         ------ borrow later used here  ^^ borrowed value does not live long enough
...
26 |     }
   |     - `i` dropped here while still borrowed

For more information about this error, try `rustc --explain E0597`.

but i is dropped after the reference has been removed from the BTreeMap.

I know this can be fixed with creating a new BTreeMap for each iteration of the loop, and passing ownership, but I'd like to simply reuse an existing one in order to save performance costs. I may be misguided, please bear with me if I'm approaching this problem incorrectly, thanks!

Have you considered generics?

impl DebugRenderer<'_> {
    fn draw_to_canvas<T: DebugTrait>(&self, values: &BTreeMap<&'static str, T>) {
        for item in values.iter() {
            println!("{0}: {1:?}", item.0, item.1);
        }
    }
}

fn main() {
    let mut values: BTreeMap<&'static str, i32> = BTreeMap::new();
    let font = Font;
    let debug_renderer = DebugRenderer { font: &font };
    for i in 1..=5 {
        values.insert("example value", i);
        debug_renderer.draw_to_canvas(&values);
        values.clear();
        println!("hey {i}");
    }
}
1 Like

i is a temporary value that is alive for only one loop iteration, so a reference to i can't be stored in a map that lives for the entire loop.

The usual solution is to store owned values in the map, which means you would change the map's value type from &dyn DebugTrait to Box<dyn DebugTrait>. When adding to the array you create the boxed/owned value with Box::new(i). (modified playground)

Edit: the generics solution posted above is better if all values in the map have the same type, and you don't need trait objects (&dyn) for some other reason. Boxing requires an extra allocation and trait objects have more limitations than generics.

1 Like

@jumpnbrownweasel @drewtato - thank you for your help! I used a solution combining both - the struct function is now generic, and my main execution loop now uses a BTreeMap of Box<dyn DebugTrait>. My use-case combines many differently typed values which all implement Debug independently, and this seems like the best solution (although sacrificing a bit of memory)

2 Likes

The compiler doesn't understand what BTreeMap::clear does.

There are tricks to get it to understand there are no borrows left. Sometimes this is used to recycle borrow-carrying Vecs, as there are some (non-guaranteed) optimizations that avoid reallocations in that case. I didn't check to see if that can also happen for BTreeMap, so the example in the playground may be worse than just having a new BTreeMap per iteration.

Speaking of which, you should measure your actual use case against the different approaches with criterion or something. Optimizing this function may not matter at all, and if it does, it's not obvious to me that boxing every dyn Debug would be cheaper than a new BTreeMap.[1][2]


  1. Owning instead of borrowing is usually the solution for long-lived structs, but this is apparently a short-lived one. ↩ī¸Ž

  2. If there actually is a performance problem here, using a Vec with a custom borrowing type and sorting that is another alternative. ↩ī¸Ž

3 Likes

Thanks for showing me this solution, I assumed there was a better solution past Boxing all my important values -- although boxing was a good alternative.

Optimizing this function is important, as this loop is responsible for rendering a frame to the screen - this DebugHandler struct is then used every frame to display important values in the GUI. Any performance improvements in the main thread are incredibly important / noticeable.

Then definitely write a benchmark, try a few different things, and track performance. Us humans are bad about predicting actual outcomes of attempted optimizations.

With that cavaet...

...I still think this is worth testing and pursuing. A tree is a lot more complicated than a vec, and (if not tiny) has many distinct allocations on its own.

1 Like