George Hilliard
George Hilliard

Reputation: 15942

Mutable borrow conflict from unrolled loop

Consider the following code (Playpen), which is meant to simply parse input from stdin and put each line and a reference to that line in a structure:

use std::io;
use std::io::BufRead;

struct Line<'a> {
    text: Box<String>,
    column: &'a str,
}

fn main() {
    let column = 1;
    let stdin = io::stdin();
    let mut lines: Vec<Line> = Vec::new();

    for line_res in stdin.lock().lines() {
        lines.push(Line {
            text: Box::new(line_res.unwrap()),
            column: "",
        });

        let line = lines.last_mut().unwrap();
        line.column = line.text.split_whitespace().nth(column)
                      .unwrap_or("");
    }
}

That is, Line::column should refer to Line::text. Note that I set column to "" initially (and modify it later) because I am not aware of a way to refer to the text element at creation time.

Unfortunately, the above code does not compile, spitting out the following very obtuse error message:

<anon>:15:3: 15:8 error: cannot borrow `lines` as mutable more than once at a time
<anon>:15       lines.push(Line {
                ^~~~~
note: in expansion of for loop expansion
<anon>:14:2: 23:3 note: expansion site
<anon>:20:14: 20:19 note: previous borrow of `lines` occurs here; the mutable borrow prevents subsequent moves, borrows, or modification of `lines` until the borrow ends
<anon>:20       let line = lines.last_mut().unwrap();
                           ^~~~~
note: in expansion of for loop expansion
<anon>:14:2: 23:3 note: expansion site
<anon>:24:2: 24:2 note: previous borrow ends here
<anon>:9 fn main() {
...
<anon>:24 }
          ^
<anon>:20:14: 20:19 error: cannot borrow `lines` as mutable more than once at a time
<anon>:20       let line = lines.last_mut().unwrap();
                           ^~~~~
note: in expansion of for loop expansion
<anon>:14:2: 23:3 note: expansion site
<anon>:20:14: 20:19 note: previous borrow of `lines` occurs here; the mutable borrow prevents subsequent moves, borrows, or modification of `lines` until the borrow ends
<anon>:20       let line = lines.last_mut().unwrap();
                           ^~~~~
note: in expansion of for loop expansion
<anon>:14:2: 23:3 note: expansion site
<anon>:24:2: 24:2 note: previous borrow ends here
<anon>:9 fn main() {
...
<anon>:24 }
          ^
<anon>:20:14: 20:19 error: cannot borrow `lines` as mutable more than once at a time
<anon>:20       let line = lines.last_mut().unwrap();
                           ^~~~~
note: in expansion of for loop expansion
<anon>:14:2: 23:3 note: expansion site
<anon>:20:14: 20:19 note: previous borrow of `lines` occurs here; the mutable borrow prevents subsequent moves, borrows, or modification of `lines` until the borrow ends
<anon>:20       let line = lines.last_mut().unwrap();
                           ^~~~~
note: in expansion of for loop expansion
<anon>:14:2: 23:3 note: expansion site
<anon>:24:2: 24:2 note: previous borrow ends here
<anon>:9 fn main() {
...
<anon>:24 }
          ^
error: aborting due to 3 previous errors

Surely this is nonsense! The line conflicts with itself, among other things. The only clue that I can see in this error is the fact that the loop is being unrolled. However, shouldn't all the borrows made in the loop expire at the end of each iteration?

What is the actual semantic issue with the above code, and what is the fix?

Upvotes: 3

Views: 597

Answers (1)

Chris Morgan
Chris Morgan

Reputation: 90712

It is not possible in Rust to have a reference to something in the same structure.

Think about it:

struct Line<'a> {
    text: Box<String>,
    column: &'a str,
}

What is 'a intended to be? The lifetime of the text field (by the way, the Box wrapping around a String is completely superfluous). You thus can’t express the type until it already exists.

If such a reference were permitted, you’d run into problems like this:

let mut line = Line { text: "foo".to_owned(), column: "" };
line.column = &self.text;
line.text = "bar".to_owned();
// Uh oh, column is now invalid, pointing to freed memory

There is no way around this while the two values are stored together; they must be stored separately. The workaround that is most likely to be suitable for your case is to store indexes, e.g. start and end indexes as (usize, usize).

Now: why these particular errors? It comes down to what 'a is being inferred as; your vector of lines is Vec<Lines<'x>> for a single lifetime 'x: each Lines instance has the same lifetime. This means that the inferred lifetime must be greater than that of the loop, and so each iteration of the loop does indeed keep a mutable reference alive, and so the line does in fact conflict with itself (or rather, a previous iteration) in that way. The loop is not unrolled—it’s just the borrows from the loop are indeed still alive.

Upvotes: 4

Related Questions