How Can a Function Return &i32?

Getting started with Rust (coming from some experience with C++ and Python), sorry for the noob question. Just got this piece of code running, but cannot figure why this is running.

fn get_largest(list: &[i32]) -> &i32 {
    // Why return a reference to i32?

    let mut largest = &list[0]; // If largest is a reference to list[0], will it modify list[0]
    for item in list {
        # Why item is assigned as an &i32 here? 
        if item > largest {
            largest = item;
        }
    }
    largest // How is the reference valid after largest is popped from the stack
}

fn main() {
    let number_list:Vec<i32> = vec![102, 34, 6000, 89, 54, 2, 43, 8];
    let result:&i32 = get_largest(&number_list);
    println!("The largest number is {result}");
    for item in number_list {
                             # Why item is assigned as an i32 here? 
                             println!("{item}")

                             } // This to prove the array stays the same. 
}

Basically, the questions are as highlighted in the comments.

  • It seems largest is a reference to the zero-th element of the array. So if largest is a mutable reference, does not it modify the array's zero-th element itself?
  • Finally, largest seems to be a local reference inside the get_largest function. As soon as the function returns, should not the reference to a local variable be void as the local variable is popped from the stack?

It's not a mutable reference. It's a mutable variable containing an immutable reference. I. e. you can mutate the variable to make it point to somewhere different, but you can't mutate the array through the reference.

largest refers to elements of list but list itself is a reference. The list elements are not local to the function, but they already existskmewhere else in memory before, and the caller just passes the reference too them, and thus they aren't popped from the stack when get_largest returns.

why is item assigned &i32

this is because of the iterators the standard library offers. The for loop syntax uses IntoIterator trait implementations to work, and the standard library provides for &[T] an iterator of &T items.

In the same way, in main you get i32 due to

type Item = T;

in this trait implementation, and this loop consumes the Vec. If you want references instead, you can write for item in &number_list.

1 Like

The function get_largest() gets an pointer itself as input. Assume, it looks something like this:

  • memory address 11: first number_list element, value 102
  • memory address 12: second number_list element, value 34
  • memory address 13: third number_list element, value 6000
  • ...and so on

When get_largest() gets called, its variable list is set to 11 (and somehow a length is passed in, but we ignore this for now).

So, when get_largest() gets called, it knows, that the first element is behind address 11, the second behind address 12 etc., because list was initially set to 11.

So by iterating the for items in list (for item in list), the variable item contains only a number, starting with 11, and incrementing in each iteration, that only points to some element.

In the same way, the functions only returns the address (number) that points to the largest element.

There seems to be some confusion about what the stack actually is. If I interpret the question correctly, it implies that you understand the slice &[i32] is a stack. This is not the case.

The stack (aka call stack) is something that you only interact with indirectly by calling functions. Each time you call a function, some space needs to be allocated on the stack (by incrementing the stack pointer) so that the function has a place to store its local variables. Returning from the function is the only way to "pop" from the stack.


As for what the slice is, in Rust it's usually spelled &[T], but you've filled the generic parameter with a concrete i32 type. It is generally thought of as a pointer to memory that contains an array of type T (in this case, type i32) and a precise size for the slice.

Note that the size in the slice is different from the size of the backing array in memory:

// An array with 10 elements
let array = [0_i32; 10];

// A slice that points at element 2, and has a size of 4
let slice = &array[2..6];

In this example, array and slice looks like this in memory:

element 0, 1, 2, 3, 4, 5, 6, 7, 8, 9
array [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ]
slice [ 0, 0, 0, 0 ]

I've aligned this specifically to visually show which part of array that slice is borrowing. Exactly these four elements numbered 2-5.

A slice can also be coerced from a vector, which does have the same size as the backing array, and the slice points at element 0:

// A vector with 10 elements
let vector = vec![0_i32; 10];

// A slice that points at element 0, and has a size of 10
// With coercion by specifying the type
let slice: &[i32] = &vector;

// A slice that points at element 0, and has a size of 10
// Without coercion, by taking an unbounded slice
let slice = &vector[..];

Now that you have a full description of what a slice is, perhaps the other answers will make more intuitive sense.


BTW, there is a pop() method on Vec<T>, since the data structure is technically stack-like. But you have to call the method to do the mutation (remove the last element). It never happens implicitly.

1 Like

There is no particular reason. Because i32 is Copy, you could copy it out of the reference and return an owned i32.

It you wanted to make the function generic though (using T: PartialOrd instead of i32), you would indeed need to return a reference, because you can't clone the values out of the slice.

No, it won't!

Rust does differenciate between assigning the reference itself and assigning the referred place through the reference.

let mut r = &mut list[0];
r = &mut list[1]; // changing r to point to list[1], without affecting list[0];
*r = 5; // assigning 5 to list[1], r still points to list[1]
*r = list[2]; // copying list[2] to list[1], r still points to list[1]

Also, as others have pointed out, you have a shared reference (&T, not &mut T), which you can not assign through (although interior mutability does allow mutation through shared references, it is not relevant here).

The for loop uses IntoIterator. list is of type &[i32], which implements IntoIterator<Item = &i32>. This is a generic impl, so the copying as described above doesn't work. Because of this, it has to give you references that point to elements in the slice.

You probably meant after list is popped from the stack?

The thing is, the function received the slice as an argument, so the slice pointed to by list is owned by the caller. Because of that, the slice [i32] pointed to by list will continue to live after the function returns, so it should be fine to return a reference to it. And it is!

It is still important though that the caller must not deallocate the slice while the reference we returned is valid. This is where lifetimes come in. The expanded signature of the function you've written is:

fn get_largest<'a>(list: &'a [i32]) -> &'a i32

The 'as tell the compiler that the returned reference is valid only as long as the reference passed as input also is, so the borrow checker tries to keep number_list borrowed until result is used.

Again, IntoIterator. This time you're calling it on a Vec<i32>, which will consume the Vec and move the elements out of it, hence IntoIterator<Item = i32>. Note that if you did

for item in &number_list {

Then item would be i32 once again.