5

Why would all these three print_max functions work? Which one is the best practice? Is for number in number_list a shortcut for for number in number_list.iter()?

fn main() {
    let number_list = vec![34, 50, 25, 100, 65];

    print_max_1(&number_list);
    print_max_2(&number_list);
    print_max_3(&number_list);
}

fn print_max_1(number_list: &[u16]) {
    let mut largest = &number_list[0]; // borrow the first number
    for number in number_list.iter() { // borrowing?
        if number > largest {
            largest = number;
        }
    }
    println!("The largest number is {}", largest);
}

fn print_max_2(number_list: &[u16]) {
    let mut largest = &number_list[0]; // borrow the first number
    for number in number_list {        // shortcut for .iter()?
        if number > largest {
            largest = number;
        }
    }
    println!("The largest number is {}", largest);
} 

fn print_max_3(number_list: &[u16]) {
    let mut largest = number_list[0];    // copying?
    for &number in number_list.iter() {  // borrowing?
        if number > largest {            // no error
            largest = number;
        }
    }
    println!("The largest number is {}", largest);
}

Why wouldn't this work?

fn print_max_4(number_list: &[u16]) {
    let mut largest = &number_list[0];
    for &number in number_list {
        if number > largest {
            largest = number;
        }
    }
    println!("The largest number is {}", largest);
} 

The error message says that largest is &u16 while number is u16. Why isn't number &u16?

Tianyi Shi
  • 883
  • 10
  • 15
  • In general, borrowing `u16` is rather silly since (ignoring any optimizer that the compiler may be performing here) you're using _more_ stack space to store a pointer that you still need to dereference to access the orignial `u16`. – Brian61354270 Feb 29 '20 at 02:45

2 Answers2

5

Let's tackle these one-by-one.

print_max_1

Here, largest is a mutable variable that holds an immutable reference to a u16 (i.e. it holds a &u16). Within the loop, number is also a &u16 which, like largest, is borrowed from number_list. Both number and larger are implicitly dereferenced to perform the comparison. If the value referenced by number is greater than that referenced by larger, you store a copy of the immutable reference contained in number into largest.

print_max_2

In this case, since number_list is itself borrowed, the analysis of print_max_2 is identical to print_max_1.

print_max_3

Here, largest is a u16. You are correct that number_list[0] is copied, but it is worth noting that this copy is cheap. Within the loop, each element of number_list is copied and stored directly in number. If number is greater than largest, you stored the new greatest value directly in largest. This is the most optimal of the three implementations you've written, since you do away with all of the unnecessary indirection that references (i.e., pointers) introduce.

print_max_4

Once again, you store a reference to the first element of number_list in largest, i.e. largest is a &u16. Similarly, as was the case in print_max_3, number is a u16, which will hold copies of the elements from number_list. However, as you noted, this function is the problem child.

Within the loop, the compiler will point out two errors:

  1. You attempt to compare two distinct types which don't have a PartialOrder defined, namely largest which is a &u16 and number which is a u16. Rust isn't in the business of trying to guess what you mean by this comparison, so in order fix this, you'll have to make both number and largest the same type. In this case, what you want to do is explicitly dereference largest using the * operator, which will allow you to compare the u16 it points to with the u16 contained in number. This final comparison looks like
    if number > *largest { ... }
    
  2. You attempt to store a u16 in a variable of type &u16, which does not make sense. Unfortunately, here you're going to run into a wall. Within the loop, all you have is the value of the number you copied from number_list, but largest needs to hold a reference to a u16. We can't simply borrow number here (e.g. by writing largest = &number), since number will be dropped (i.e. go out of scope) at the end of the loop. The only way to resolve is is to revert by to print_max_2 by storing the maximum value itself instead of the pointer to it.

As for whether for number in number_list is a shortcut for for number in number_list.iter(), the answer is a firm no. The former will take ownership of number_list, and during each iteration, number takes ownership of the next value in number_list. In contrasts, the latter only performs a borrow, and during each iteration of the loop, number receives an immutable reference to the next element of number_list.

In this specific case, these two operation appear identical, since taking ownership of an immutable reference simply entails making a copy, which leaves the original owner intact. For more information, see this answer to a related question on the difference between .into_iter() and .iter().

Brian61354270
  • 8,690
  • 4
  • 21
  • 43
  • I think llvm would optimize all of these equally, pointers or not? Would be really surprised to see codegen differences in godbolt – Chris Beck Feb 29 '20 at 03:56
  • @Brian Could you give some rust-doc references for the explanation of `print_max_1()`: "Both `number` and `largest`, (are `&u16`, and) are implicitly dereferenced to perform the comparison."? Thanks. – mitnk Feb 29 '20 at 08:12
  • @mitnk See [this `impl`](https://doc.rust-lang.org/src/core/cmp.rs.html#1178-1191) from `cmp.rs` in the standard library. Since you asked for a doc reference, other relelvant reading may include TRPL 2018, [15.2 Implicit Deref Coercions with Functions and Methods](https://doc.rust-lang.org/1.29.0/book/2018-edition/ch15-02-deref.html#implicit-deref-coercions-with-functions-and-methods) (not in play in here), and similarly, the [`Deref`](https://doc.rust-lang.org/std/ops/trait.Deref.html) and [`PartialOrd`](https://doc.rust-lang.org/std/cmp/trait.PartialOrd.html) traits. – Brian61354270 Mar 01 '20 at 16:49
1

There are a few things happening auto-magically here to note:

Your variable 'number_list' is a std::vec::Vec. You then use a slice for the function argument signatures. Vector has an implementation for the Deref trait. In rust, this particular arrangement uses Deref coercion to convert the vector with mentioned Deref trait into a std::slice.

However, both vectors and slices can be iterated by using a for loop. Anything that implements the std::iter::Iterator trait. The vector doesn't do this, but rather implements std::iter::IntoIterator, which as it puts it, By implementing IntoIterator for a type, you define how it will be converted to an iterator. This is common for types which describe a collection of some kind. Take a look at Implementing an Iterator for more details. Specifically the lines:

all Iterators implement IntoIterator, by just returning themselves. This means two things:

If you're writing an Iterator, you can use it with a for loop. If you're creating a collection, implementing IntoIterator for it will allow your collection to be used with the for loop.

You'll find that Rust provides a lot of conversion traits, and some automatic hidden behavior. With the extensive type use in Rust, this helps alleviate some of the necessary conversions. See From and Into for more details there.

I hated this about C++ (hidden code), however it's not so bad in Rust. If the compiler lets you do it, then you've probably found what you need. Sometimes, the automatic way may not be adequate, and you may need to use supporting methods/functions to get there.

mcarton
  • 27,633
  • 5
  • 85
  • 95
James Newman
  • 105
  • 7