Question re for loop ier and tuples

Hello everyone

I am learning Rust, reading Rust in Action book now (looks good so far btw).
Using Rust 1.53.0 Stable on Windows 10, 64-bit.

I have a minimal code fragment below which puzzles me.

Two questions, please, annotated in // Comments in code

Please ignore the actual logic, its a code fragment based on code in the book I am reading.

fn main() {
    let ctx: Vec<Vec<(usize, String)>> = Vec::new();
    for local_ctx in ctx.iter() {
        for &(i, ref line) in local_ctx.iter() { // QUESTION 1, loop 1, note: & and ref used for tuple here
            let line_num = i + 1; // i is usize, no deref needed here, expected
            println!("(A){}: {}", line_num, line); // ref line is &String, expected
        }
    }
    for local_ctx in ctx.iter() {
        for (i, line) in local_ctx.iter() { // QUESTION 1, loop 2, & and ref removed from tuple, what is best?
            let line_num = *i + 1; // QUESTION 2, deref of i, so *i works as expected
            let line_number = i + 1; // QUESTION 2, no deref of i, compiles & works correctly, why?
            println!("(B){}: {} {}", line_num, line_number, line); // line is &String, even though ref is not used in for loop
        }
    }
}

More information:
Both loops perform identically re tuple processing so I wonder which tuple binding is more idiomatic Rust
I put // comments in code fragment to help with understanding of my questions.

The FOR loop #1 uses &(i, ref line) and FOR loop 2 uses: (i, line) tuples.
Which is more correct or better use in Rust?
Both return references to &String for line variable but var i is different, it is usize in loop 1, versus &usize in loop 2, that is my question 2 re use of *deref for i.

Question 2: in second loop, i is a &usize, so *i + 1 works, as I expected. What is puzzling for me the following line of code uses i + 1, without *deref for i and it also works correctly. I expected it to give me compilation error (so I am forced to *deref i,) but it appears that in context of for loop both i and *i deref are treated identically. Why so?

Many thanks, I hope I have explained it sufficiently well.

There are implementations that let you add not only usize + usize but also

For convenience in cases just like this.

2 Likes

thank you, so *i + 1 is same as i + 1?

I just tried it, it is not just for usize, this code works for i32 also.

    let a: &i32 = &10;
    let b = a + 1;
    let c = *a + a;
    let d = *a + 1;
    println!("{} {} {}", b, c, d);

no errors, works correctly, no panics.

So when do I have to use the *deref with reference variables & ??

Or alternatively, which types allow me to choose to use either deref *i or simple i.
And what is best practice/idiomatic Rust?
I am aware of automatic deref when calling functions with parameters but this looks like entirely different case to me. I have not seen it documented anywhere but I am sure it is.

thanks for the speedy reply, very impressive.

It's not automatic deref, there are different functions being called (at least semantically, with integer addition it might really be an intrinsic under the hood). I'm not aware of any specific documentation of this other than all those trait implementations.

This is easier to explore if you make your own types. In this playground I made three types to help illustrate things:

  • SimpleAdd which you can only add to another SimpleType
    • As if you could only do usize + usize
  • WeirdAdd, where you can only add a referenced one to a non-referenced one
    • As if you could only do &usize + usize -- and could not even usize + &usize
  • NormalAdd, that has all four implementations, like usize and the others do

You can try playing around with them, e.g. uncomment some functions to see that there is no deref going on as they don't compile, change the order of addition for WeirdAdd, add print statements to the different fn add()s, etc.

If you change the examples that don't compile to use i.add(...) instead, deref does apply, so they will compile.

2 Likes

Thank you very much, I will do so ASAP.

So it short summary, it looks like it's a matter of Rust operator overloading and trait behaviors on the Add(+) and the associated right/left hand sides of expressions.

I looked at std::ops where all these are listed, which you linked to, thank you!

Regarding your first question, there are two different mechanisms involved here.

Both loops pattern-match &(usize, String). Let's decompose it.

The first loop is:

&(i, ref line) = &(usize, String)
(i, ref line) = (usize, String) // `&` in pattern matching is the deref operator, cancelling the effect of `&` in the expression we match against
i = usize, ref line = String
i = usize, line = &String // `ref` in pattern matching is the address-taking operator, cancelling the effect of `*` in the expression we match against. You can think about `String` as `*&String`. `ref` cancels the effect of the asterisk, leaving you with `&String`.

The second loop, however, uses a feature named match ergonomics. It allows you to easily decompose references when matching against tuples, structs or slices. For example:

let MyStruct { i } = &MyStruct { i: 1 };
// Same as:
let &MyStruct { ref i } = &MyStruct { i: 1 };

let (i, j) = &(1, 2);
// Same as:
let &(ref i, ref j) = &(1, 2);

if let [i, j] = &[1, 2] { ... }
// Same as:
if let &[ref i, ref j] = &[1, 2)] { ... }

Thus, the pattern in the second loop is:

(i, line) = &(usize, String)
&(ref i, ref line) = &(usize, String) // Match ergonomics
(ref i, ref line) = (usize, String)
ref i = usize, ref line = String
i = &usize, line = &String
1 Like

thank you very much!

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.