Migwell
Migwell

Reputation: 20137

Conditionally borrowing in Rust, while keeping the value in scope

I have a Rust function that can take a slice, but also has a default value. Then, if the user passes None, I want to generate a default value. However, because the data type I expect is a slice, I have to borrow that default value, but it immediately goes out of scope. How can I conditionally create an owned value and then borrow it, considering that adding a condition always introduces a new scope that will then drop that value once it ends?

fn with_default(foo: usize, bar: Option<&[usize]>) {
    let bar: &[usize] = match bar {
        Some(s) => s,
        None => { 
            let v: Vec<usize> = (0..foo).collect();
            &v
        }
    };
}
            &v
            ^^ borrowed value does not live long enough
        }
        ^ `v` dropped here while still borrowed
    let bar: &[usize] = match bar {
        ^^^ borrow later stored here

Upvotes: 4

Views: 476

Answers (3)

Stargateur
Stargateur

Reputation: 26757

In Rust on your case I would always advice a little factorisation:

pub fn with_default(foo: usize, bar: Option<&[usize]>) {
    fn aux(foo: usize, bar: &[usize]) {
        println!("{:?} {:?}", foo, bar);
    }

    match bar {
        Some(bar) => aux(foo, bar),
        None => aux(foo, &(0..foo).collect::<Vec<_>>()),
    }
}

fn main() {
    with_default(5, Some(&[1, 2, 3]));
    with_default(5, None);
}

This have no problem:

  • clear use structural pattern
  • avoid Silvio solution that have an overhead
  • avoid your solution that have an overhead

you can declare aux() everywhere but that a pattern I often use and so I put it inside the function itself, on "auxiliary" helper that do the logic.

Detail about the overhead you use:

Rustc is not smart enough (at least for now) to generate at compile time a perfect code so a "possibly init" variable will need a boolean runtime to know if rust need to call the drop implementation, on the case of Silvio solution that almost the same cause that need a branch to check state value of the option.

Plus: I hate the pattern of not initialize a variable I think Silvio solution way more clean on this matter.

Upvotes: 4

Migwell
Migwell

Reputation: 20137

Inspired by Silvio Mayolo's answer, I realised that I can declare the Vec in the top scope, and only assign + borrow it conditionally:

fn with_default(foo: usize, bar: Option<&[usize]>) {    
    let temp_vec: Vec<usize>;
    let bar: &[usize] = match bar {    
        Some(s) => s,    
        None => {                     
            temp_vec = (0..foo).collect(); 
            &temp_vec
        }   
    };
}

Here's a playground link showing it working as intended: https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=3bd817cf2ffa10c131445c19be16cbfe

Upvotes: 7

Silvio Mayolo
Silvio Mayolo

Reputation: 70297

If you need the vector to last only as long as the function with_default is running (i.e. it doesn't need to be returned from the function or otherwise outlive it), then you just need to make some stack space in the function for the vector. Since it won't always be populated, we'll use Option<Vec<usize>>.

fn with_default(foo: usize, bar: Option<&[usize]>) {

  // Somewhere to store the temporary vector. This variable
  // is owned by the function and will be freed when it goes
  // out of scope.
  let mut my_vec: Option<Vec<usize>> = None;

  let bar: &[usize] = match bar {
    Some(s) => s,
    None => {
      my_vec = Some((0..foo).collect());
      // We know my_vec is a Some now, so unwrap() is safe. But
      // unwrap() would move the value out of the Option, which
      // defeats the point. Option::as_ref() turns &Option<T> into
      // Option<&T>, allowing us to borrow the inside of the Option.
      my_vec.as_ref().unwrap()
    }
  };
}

Upvotes: 3

Related Questions