Error with function return value

The following code does not compile and gives the error: error[E0515]: cannot return value referencing local variable 'map'

use std::collections::HashMap;

#[derive(Debug)]
struct Ctx<'a> {
    map: HashMap<u32, String>,
    vec: Vec<&'a str>,
}

fn mk_ctx<'a>() -> Ctx<'a> {
    let map: HashMap<u32, String> = HashMap::from([(0, "foo".into())]);
    let vec: Vec<&str> = mk_vec(&map);

    Ctx { map, vec }
}

fn mk_vec(map: &HashMap<u32, String>) -> Vec<&str> {
    todo!()
}

fn main() {
    let res = mk_ctx();
    println!("{res:?}")
}

Sandbox

As far as I understand due to map being a parameter to mk_vec and by applying lifetime elision rules, the &str in vec will have the same lifetime as map. At the end of mk_ctx map is moved to ctx.map. I would assume that vec can be moved to ctx.vec as well.

What is the issue here and how can the code be fixed?

You are trying to create a self-referential struct, which are borderline useless types and therefore best to be avoided. If you can't find a better abstraction for your problem, libraries like ouroboros and yoke provide safe abstractions for creating usable self-referential structs.

2 Likes

There seems to be also another issue? This part:

    let map: HashMap<u32, String> = HashMap::from([(0, "foo".into())]);
    let vec: Vec<&str> = mk_vec(&map);

    Ctx { map, vec };

Is moving map while it is borrowed (by vec).

Your analysis is correct. Just specify the lifetimes and the code will compile.

Bear in mind what @jofas said about creating a self-referential struct.

The mk_vec function as you have written needs to return string slices with an unbounded lifetime 'b. Effectively the only way to implement mk_vec is returning either an empty Vec or a Vec of &'static strs, both of which are not reasonable.

Whenever you think that specifying some lifetime annotation will make a piece of code compile you also need to make sure that other pieces of code will be able to compile, otherwise you're just moving some unreasonable constraints from one place to the other, ultimately just wasting your time.

Yes. I only confirmed the OPs analysis, hence the warning about creating a self-referential struct.

@firebits.io Thanks, but specifying the lifetimes does no longer work with this implementation of mk_vec:

use std::collections::HashMap;

#[derive(Debug)]
struct Ctx<'a> {
    map: HashMap<u32, String>,
    vec: Vec<&'a str>,
}

fn mk_ctx<'a>() -> Ctx<'a> {
    let map: HashMap<u32, String> = HashMap::from([(0, "foo".into())]);
    let vec: Vec<&str> = mk_vec(&map);

    Ctx { map, vec }
}

fn mk_vec<'a, 'b>(map: &'a HashMap<u32, String>) -> Vec<&'b str> {
    map.values().map(|s| s.as_str()).collect()
}

fn main() {
    let res = mk_ctx();
    println!("{res:?}")
}

Plauground

Would the following version be a valid solution?

use std::collections::HashMap;
use std::rc::Rc;

#[derive(Debug)]
struct Ctx {
    map: Rc<HashMap<u32, String>>,
    vec: Vec<Rc<str>>,
}

fn mk_ctx() -> Ctx {
    let map: Rc<HashMap<u32, String>> = Rc::new(HashMap::from([(0, "foo".into())]));
    let vec = mk_vec(Rc::clone(&map));

    Ctx { map, vec }
}

fn mk_vec(map: Rc<HashMap<u32, String>>) -> Vec<Rc<str>> {
    map.values().map(|s| Rc::from(s.as_str())).collect()
}

fn main() {
    let res = mk_ctx();
    println!("{res:?}")
}

Playground

Yes. Using reference counting is a common workaround for this type of problems.

I'm not sure how that confirms OP's analysis. From what I see their analysis is:

  • given the lifetime elision in mk_vec, vec borrows from map
  • map is moved in the Ctx
  • hence vec should be able to be moved too

This is incorrect, because the last point is wrong. Your playground however changes the assumption in the first point by making vec no longer borrow from map. This changes everything and makes any conclusion useless

2 Likes

Doing this will not avoid copies compared to a solution where the values are simply copied (for example Rust Playground).

If you want to avoid copying the values of the HashMap then you will have to wrap those in a Rc, not the HashMap itself. Rust Playground

Fair point. I don't remember seing that point, I just quickly skimmed through the document and read that the OP's analysis was about lifetime elision.

Lifetimes on structs refer to data in a larger scope, one that is not in the struct.

This is the "outlives" relationship. The data borrowed for the scope 'a may exist before Ctx<'a> is created, and keep existing after Ctx<'a> is destroyed. This means the temporarily borrowed data can't be stored in the struct itself.

1 Like

Reference counting is good. Another way is to store the HashMap outside of the Ctx, like this:

struct Base {
    map: HashMap<u32, String>,
}

struct Ctx<'a> {
    map: &'a HashMap<u32, String>,
    vec: Vec<&'a str>,
}

playground

2 Likes

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.