When returning the outcome of consuming a StdinLock, why was the borrow to stdin retained?

Given the following function:

use std::io::{BufRead, stdin};

fn foo() -> usize {
    let stdin = stdin();
    let stdinlock = stdin.lock();
    stdinlock
        .lines()
        .count()
}

This fails to compile with the following error:

error: `stdin` does not live long enough
  --> src/main.rs:12:1
   |
7  |     let stdinlock = stdin.lock();
   |                     ----- borrow occurs here
...
11 | }
   | ^ `stdin` dropped here while still borrowed
   |
   = note: values in a scope are dropped in the opposite order they are created

I find this surprising because the outcome of consuming the lock (via lines) does not retain any references to the original source. In fact, assigning the same outcome to a binding before returning works just fine (Playground).

fn bar() -> usize {
    let stdin = stdin();
    let stdinlock = stdin.lock();
    let r = stdinlock
        .lines()
        .count();
    r
}

This suggests that returning a "consumed lock" immediately has led to the lock attempting to live longer than the locked content, much in an unusual way. All references that I looked into usually point out that the order of declaration matters, but not how the returned objects can affect the order in which they are released.

So why is the former function rejected by the compiler? Why is the lock being seemingly retained for longer than expected?


Solution 1:

This seems to be a bug in the compiler. You can make the compiler happy by using an explicit return statement:

use std::io::{stdin, BufRead};

fn foo() -> usize {
    let stdin = stdin();
    let stdinlock = stdin.lock();
    return stdinlock
        .lines()
        .count();
}

fn main() {}

playground

As mentioned in the comments, there are multiple Rust issues related to this:

  • 37407
  • 21114

Solution 2:

I cannot answer the why of your question, but I can state that the current1 implementation of non-lexical lifetimes allows the original code to compile:

#![feature(nll)]

use std::io::{BufRead, stdin};

fn foo() -> usize {
    let stdin = stdin();
    let stdinlock = stdin.lock();
    stdinlock
        .lines()
        .count()
}

Playground

1 1.25.0-nightly (2018-01-11 73ac5d6)