Why must I mutably borrow struct if method result is while loop condition?

As a disclaimer, this is day#1 Rust, but I come from a C/C++/D background.

First of all, what a wonderful documentation and support ecosystem. Still, the concept of ownership and borrowing has a lot of corners to explore and understand probably through experience only.

In addition to exercises in The Book, I am working on practical problems related to my work. One of these is linewise reading and parsing of (potentially) GByte sized files.

My first attempt uses the Lines iterator returned by BufReader::lines() and is thus inefficient in terms of heap alloc:

use std::io::{self,BufRead};
use std::fs::File;

fn main() -> std::result::Result<(), std::io::Error> {
    let filename = "test.bed";
    let fi = File::open(filename)?;

    let bedreader = io::BufReader::new(fi);

    for line in bedreader.lines() {
        println!("{}", line?);
    }

    Ok(())
}

In order to reuse a preallocated buffer, it looks like I can use read_line directly:

    let mut line = String::new();

    while bedreader.read_line(&mut line).unwrap() > 0 {
        println!("{}", line.trim_end());
        line.clear();
    }

but the above code compiles only if bedreader is defined as mutable:

11 |     while bedreader.read_line(&mut line).unwrap() > 0 {
   |           ^^^^^^^^^ cannot borrow as mutable

Question: Why is this? in both cases, the BufReader instance is not mutated nor does it need to be; it is not updated as a part of the while condition; only a member function return value changes. In the case of the iterator, it (bedreader: BufReader) does not need to be defined as mut.

Could someone please explain in more detail why the borrow checker gripes about this? Is it related to the fact that I passed a mutable reference (&mut line) to the member function?

Thank you so much in advance

bedreader.read_line() mutates bedreader. The documentation for read_line can be found here on the BufRead trait that provides it, where you will see that it takes &mut self.

(on the original BufReader doc page, the docs for read_line are (unhelpfully) hidden behind two collapsed [+] icons, under the BufRead implementation)

It mutates the BufReader in the following ways:

  • The inner reader (the File) is "mutated" by the BufReader to read bytes. (I put air quotes because, technically, no userland data is modified and you're allowed to read from &File due to thread safety in the underlying APIs and yadda yadda yadda; but BufReader is a generic type and does not know this)
  • If the BufReader has unused bytes remaining in its buffer, then these bytes will be read into the String first. So at the very least, the BufReader must somehow mutate itself to indicate that these bytes have been consumed.
  • (it also almost certainly fills the buffer when empty rather than just reading directly to the String, because... that's kind of the point of having buffered reads!)
1 Like

Got it (and thank you for the links; also I noticed in the right margin a src link to look closer at implementation details, which was helpful.

So, looking also at the implemented methods on Lines, fn next also takes &mut self, which makes sense as surely the iterator must update its internal state (which in this case is by also calling read_line.

Followup questions:

  1. it appears that BufRead::lines() returns something (Lines) which must be mutable and this fact is inferred by the compiler. Is there anywhere to make that explicit, constrain it, observe it, etc.?

  2. With respect to the Lines iterator, is it the case that bedreader may remain non-mut in that case because the Lines iterator has taken ownership of buf ?

Thank you again!

You can expand the for loop yourself. The full desugaring is in the docs, but this simplified version might be easier to play with:

    let mut iter: Lines<_> = bedreader.lines().into_iter();
    while let Some(line) = iter.next() {
        println!("{}", line?);
    }

In this playground you can see what happens if you change which variables are mut.

1 Like

In Rust, all temporaries are implicitly mutable, because no other code can possibly see the value or be affected by the mutation. It is only once you store something to a local (using a let statement) that the compiler begins to care whether you have annotated it as mut.

You can even do something like:

let x = vec![2, 1, 3];
let mut y = x;   // move x to another local and change its mutability
y.sort();

Notice that the requirement of writing mut on mutated let bindings is only a lint at best (granted, an always-forbidden lint), and does not play a direct role in any of Rust's memory safety mechanisms. Those are all in the type system, lifetimes, and the borrow checker.

With respect to the Lines iterator, is it the case that bedreader may remain non-mut in that case because the Lines iterator has taken ownership of buf ?

Correct. If you call bedreader.lines(), bedreader does not need to be mut.

1 Like