A lifetime issue pops up after a seemingly ineffectual change

Viewed 69
#![allow(dead_code)]
#![allow(unused_variables)]

use std::cell::Cell;

pub struct Foo<'a> {
    pub x1: &'a i32,
    pub x2: &'a i32,
    pub data: Cell<&'a i32>,
}

fn test<'a>(foo: &Foo<'a>) {
    let x1 = 1;
    let data = Cell::new(foo.data.get());
    // UNCOMMENT THE NEXT LINE
    // let data = foo.data.clone();
    Foo {
        x1: &x1,
        x2: foo.x2,
        data,
    };
}

The above code compiles, but if you uncomment the line I marked in the code (and optionally comment out the line before it), you will see the following error (I use Rust 1.58.1):

error[E0597]: `x1` does not live long enough
  --> src/lib.rs:18:13
   |
12 | fn test<'a>(foo: &Foo<'a>) {
   |         -- lifetime `'a` defined here
...
18 |         x1: &x1,
   |             ^^^
   |             |
   |             borrowed value does not live long enough
   |             this usage requires that `x1` is borrowed for `'a`
...
22 | }
   | - `x1` dropped here while still borrowed

For more information about this error, try `rustc --explain E0597`.

The line that causes error when uncommented strongly seems to have the same effect as the line before it - I basically inlined the clone implementation; quote from the rust stdlib source:

#[stable(feature = "rust1", since = "1.0.0")]
impl<T: Copy> Clone for Cell<T> {
    #[inline]
    fn clone(&self) -> Cell<T> {
        Cell::new(self.get())
    }
}

Question: where the difference comes from?

1 Answers

When you write

    let data = Cell::new(foo.data.get());

the value foo.data.get() has type &'a i32. This reference can then be implicitly reinterpreted as having any lifetime no longer than 'a. In particular, we can take it as the lifetime I'll call 'f, an actually-unnameable lifetime which is outlived by 'a and the scope of the local x1. Then we call Cell::new to construct a local Foo<'f>.

On the other hand, if data is a clone of data from Foo, then data must have the exact type Cell<&'a i32>, and because Cell is interior mutable, its type parameter is invariant, so the Cell<&'a i32> cannot be reinterpreted as containing a reference with a shorter lifetime, and so the Foo must be Foo<'a> — but x does not outlive 'a so &x1 cannot be stored in the Foo<'a>.


You write that you “inlined the clone implementation”, and wonder why this makes a difference; the key differences are that the inlined version

  1. is aware that it is dealing with a shared reference, rather than an opaque T in Cell<T> (so it can use shortening/reborrowing/covariance), and
  2. does not force the input and output to be the same type (including lifetimes).

To illustrate this, here's a rewritten version with two functions with identical bodies, but different signatures. This compiles, but if you substitute clone_cell for rebuild_cell it won't compile.

use std::cell::Cell;

// Type inside the Cell is opaque and will remain unchanged.
fn clone_cell<T: Copy>(cell: &Cell<T>) -> Cell<T> {
    Cell::new(cell.get())
}

// Input and output lifetimes are separated, and have a relationship
// but are not necessarily equal.
fn rebuild_cell<'input, 'output>(cell: &Cell<&'input i32>) -> Cell<&'output i32>
where
    'input: 'output
{
    // When the data flows from cell.get() to Cell::new(), the compiler 
    // notices that the shared reference can be coerced from &'input i32
    // to &'output i32. Thus, 'output is allowed to be shorter, to satisfy
    // any other lifetime requirement imposed on the output cell.
    Cell::new(cell.get())
}

fn test<'a>(foo: &[Cell<&'a i32>; 2]) {
    let x1 = 1;
    let _ = [rebuild_cell(&foo[0]), Cell::new(&x1)];
}

Even in common uses of the standard library, Clone::clone does not always have the signature one would like; for example, the ToOwned trait exists to allow a change of type. Unfortunately, there's no general way to apply that technique to problems like this, since there's no way to express “allow the lifetimes anywhere in this type to become flexible” as a trait signature.

Related