3
votes

I'm writing a function that takes a reference to an integer and returns a vector of that integer times 2, 5 times. I think that'd look something like:

fn foo(x: &i64) -> Vec<&i64> {
    let mut v = vec![];
    for i in 0..5 {
        let q = x * 2;
        v.push(&q);
    }
    v
}

fn main() {
    let x = 5;
    let q = foo(&x);
    println!("{:?}", q);
}

The borrow checker goes nuts because I define a new variable, it's allocated on the stack, and goes out of scope at the end of the function.

What do I do? Certainly I can't go through life without writing functions that create new data! I'm aware there's Box, and Copy-type workarounds, but I'm interested in an idiomatic Rust solution.

I realize I could return a Vec<i64> but I think that'd run into the same issues? Mainly trying to come up with an "emblematic" problem for the general issue :)

1
but I think that'd run into the same issues — did you try this? Why do you think it would have the same issues?Shepmaster
@Shepmaster I assumed if I allocated the i64's in the new function, on the stack, it would get pissed. But it seems that it works. I'm guessing this is because I am giving ownership back to the caller, so the borrow checker doesn't get upset, i.e. there's no borrow? Is that kind of solution more idiomatic than using a containing heap-allocated structure like Box or Cell?William
You are correct that there is no borrowing involved: you allocate the i64s on the stack, then transfer ownership of them to the Vec, and then transfer ownership of the Vec back to the caller. more idiomatic than using a containing heap-allocated structure — a Vec is a "containing heap-allocated structure". I would say it is more idiomatic to use a Vec in this case than a Box or Cell. In this specific example, you could also return an array [i64; 5] since you know how many numbers you will be returning.Shepmaster

1 Answers

5
votes

EDIT: I only just realized that you wrote "I'm aware there's Box, Copy etc type workaround but I'm mostly interested in an idiomatic rust solution", but I've already typed the whole answer. :P And the solutions below are idiomatic Rust, this is all just how memory works! Don't go trying to return pointers to stack-allocated data in C or C++, because even if the compiler doesn't stop you, that doesn't mean anything good will come of it. ;)


Any time that you return a reference, that reference must have been a parameter to the function. In other words, if you're returning references to data, all that data must have been allocated outside of the function. You seem to understand this, I just want to make sure it's clear. :)

There are many potential ways of solving this problem depending on what your use case is.

In this particular example, because you don't need x for anything afterward, you can just give ownership to foo without bothering with references at all:

fn foo(x: i64) -> Vec<i64> {
    std::iter::repeat(x * 2).take(5).collect()
}

fn main() {
    let x = 5;
    println!("{:?}", foo(x));
}

But let's say that you don't want to pass ownership into foo. You could still return a vector of references as long as you didn't want to mutate the underlying value:

fn foo(x: &i64) -> Vec<&i64> {
    std::iter::repeat(x).take(5).collect()
}

fn main() {
    let x = 5;
    println!("{:?}", foo(&x));
}

...and likewise you could mutate the underlying value as long as you didn't want to hand out new pointers to it:

fn foo(x: &mut i64) -> &mut i64 {
    *x *= 2;
    x
}

fn main() {
    let mut x = 5;
    println!("{:?}", foo(&mut x));
}

...but of course, you want to do both. So if you're allocating memory and you want to return it, then you need to do it somewhere other than the stack. One thing you can do is just stuff it on the heap, using Box:

// Just for illustration, see the next example for a better approach
fn foo(x: &i64) -> Vec<Box<i64>> {
    std::iter::repeat(Box::new(x * 2)).take(5).collect()
}

fn main() {
    let x = 5;
    println!("{:?}", foo(&x));
}

...though with the above I just want to make sure you're aware of Box as a general means of using the heap. Truthfully, simply using a Vec means that your data will be placed on the heap, so this works:

fn foo(x: &i64) -> Vec<i64> {
    std::iter::repeat(x * 2).take(5).collect()
}

fn main() {
    let x = 5;
    println!("{:?}", foo(&x));
}

The above is probably the most idiomatic example here, though as ever your use case might demand something different.

Alternatively, you could pull a trick from C's playbook and pre-allocate the memory outside of foo, and then pass in a reference to it:

fn foo(x: &i64, v: &mut [i64; 5]) {
    for i in v {
        *i = x * 2;
    }
}

fn main() {
    let x = 5;
    let mut v = [0; 5];  // fixed-size array on the stack
    foo(&x, &mut v);
    println!("{:?}", v);
}

Finally, if the function must take a reference as its parameter and you must mutate the referenced data and you must copy the reference itself and you must return these copied references, then you can use Cell for this:

use std::cell::Cell;

fn foo(x: &Cell<i64>) -> Vec<&Cell<i64>> {
    x.set(x.get() * 2);
    std::iter::repeat(x).take(5).collect()
}

fn main() {
    let x = Cell::new(5);
    println!("{:?}", foo(&x));
}

Cell is both efficient and non-surprising, though note that Cell works only on types that implement the Copy trait (which all the primitive numeric types do). If your type doesn't implement Copy then you can still do this same thing with RefCell, but it imposes a slight runtime overhead and opens up the possibilities for panics at runtime if you get the "borrowing" wrong.