"Value dropped while borrowed" as a lifetime mismatch

I was reading this question and it got me thinking: how can that issue be explained as a lifetime mismatch?

fn main() {
    let longest;

    let string1 = String::new();
    let str1 = &string1;
    {
        let string2 = String::new();
        let str2 = &string2;
        longest = longer(str1, str2);
    }
    println!("{str1}");
    println!("{longest}");
}

fn longer<'a>(str1: &'a str, str2: &'a str) -> &'a str {
    str2
}

In the 'nomicon, it states that specifying an input lifetime and an output lifetime that are the same is saying that the function can produce a reference to a value of the output type that lives just as long, or longer, than the value behind the input reference(s). This doesn't really make sense: which input referent must the output one live as long as? If it must live as long as *str2, then it does, but still complains. If it must live as long as str1, then the below example wouldn't pass the borrow check, but it does. If it means that *str1 and *str2 must live for the same time, then that means that this wouldn't be viable:

fn main() {
    let longest;

    let string1 = String::new();
    let str1 = &string1;
    {
        let string2 = String::new();
        let str2 = &string2;
        longest = longer(str1, str2);
        println!("{longest}");
    }
    println!("{str1}");
    
}

fn longer<'a>(str1: &'a str, str2: &'a str) -> &'a str {
    str2
}

but it is, because, obviously, longest doesn't outlive *str1 or *str2. But that isn't explained by the 'nomicon.

This makes sense to me:

fn longer<'a>(str1: &'a str, str2: &'b str) -> &'a str
where 'b: 'a
{
    str2
}

used in the first snippet, where it doesn't compile, because we explicitly say that 'b must live as long or longer than 'a, but that is untrue, making the borrowck upset. And this makes perfect sense to me.

However, it's the first snippet that I don't know how to explain as a breakage of what we promised with specifying the lifetimes.

Any help is greatly appreciated!

Thanks.

It might feels a bit nitpicky, but you should distinguish the notion of how long a reference is valid for from how long the value behind it is valid for. They are related, but a reference can be valid for less than what the value behind it is.

What happens when you call longer is that the validity of one of the two references is shortened to make the two input lifetimes equal. The validity of the referenced value does not change though! Then the output lifetime is guaranteed to be valid for at least as long as the input lifetimes.

However when you don't go through longer this shortening does not happen and the borrow checker can reason with the original validity.

I'll assume you're talking about the function longer, even though you're not calling it in your example. :wink:

To add to what was already said: the lifetime annotations ('a) don't represent the actual lifetimes of the references, and even less the lifetimes of the referenced objects. They're used to express a relationship between references, which allows the borrow checker to verify if the corresponding constraint can be satisfied.

Here, in the function signature, it means that str1 and str2 should live at least as long as the output of longer, and at least as long as that function. Said otherwise, there must exist a scope 'a such that all three references live in that scope.

So it doesn't mean that the variables str1 and str2 should have the same lifetime in order to be valid parameters of that function.

In the example:

Where longer is (I assume) called, its output must live until the end of main, where it's used: that's the constraint on 'a for the output. But str2 can live until the end of the inner block only, so the constraint for that input tells that 'a can only live that long. Those two constraints are incompatible.

Specifying that 'b must outlive 'a doesn't really add anything to the constraints, nor does it clarify them. I wonder if that wouldn't even be a problem if the references couldn't satisfy this extra condition, but I don't think so since what matters is that 'a lives long enough for the output. For example, this compiles just fine.

Whoops. Yes, I am talking about longer.

Thank you for the explanation (thanks to @SkiFire13 too). I understand now; I was trying to apply a model that wasn't the case.

I know it doesn't make a real difference, and I know it still doesn't compile, but it made it far easier for me to understand why it doesn't compile.

Thanks again!

Both. The str1 and str2 arguments are declared with the same lifetime. str1-the-argument uses a shorter lifetime than str1-the-local-variable, which is allowed due to subtyping. The lifetime of the return value therefore matches the lifetime of both arguments (and that of str2-the-local-variable, which did not need to be coerced).

To make this more clear:

let str1 = new Cat();
let str2 = new Animal();
let longest = longer(str1, str2);
// ^^ the only way the above type-checks is if
//   `longest` has type `Animal`, not `Cat`

function longer<T>(str1: T, str2: T): T {
	return str2;
}

EDIT: Now I'm imagining all of the borrow checker error messages but instead of complaining about lifetimes they're about cats. `str2` does not meow enough, binding `str2` declared here as `Animal`, used here as `Cat`

I might be wrong and would appreciate any corrections about how it actually works, but from what I read:

fn main() {
    let longest: &str;

    let string1 = String::new();
    // Until last usage of `str1` (while this borrow lives), `string1`
    // must remain in place and unchanged.
    let str1 = &string1;

    {
        let string2 = String::new();

        // Until last usage of `str2` (while this borrow lives), `string2`
        // must remain in place and unchanged.
        let str2 = &string2;

        // Because `fn longer`'s output has same lifetime as its inputs,
        // `str1` and `str2` must be alive for (at least*) how long
        // `longest` lives.
        // (*) note that function arguments are reborrowed automatically
        //     which can shorten the lifetime of inputs to make them match
        longest = longer(str1, str2);

        // `string2` goes out of scope, any borrow of it must not be alive
        // so `str2` borrow can live at most up to here
        // std::mem::drop(string2);
    }
    println!("{str1}");     // `str1` must live at least up to here
    println!("{longest}");  // `longest` must live at least up to here
}

fn longer<'a>(str1: &'a str, str2: &'a str) -> &'a str {
    str2
}

Then, note that str2 borrow must both outlive the block (and live till the println!) and not outlive it (because string2 it borrows from is destroyed). This set of constraints is not satisfiable, which is why borrow checker rejects the code.

In case it helps develop your mental model, I'll add my own explanation and then highlight some differences from what you may have read elsewhere.

I'll approach this by explaining the code with my mental model as soon as possible, and then try to explain the mental model in more detail afterwards.

(I tried to make this shorter but failed, and at least this way the perhaps the benefits will be apparent up front, and you can stop reading if your eyes glaze over.)


Here are the core ideas needed for the example:

  • Creating a reference to a variable makes it be borrowed
  • Using a reference keeps some set of borrows active
  • Going out of scope and other uses of variables conflict with being borrowed

And we'll also need to know the meaning of this signature:

fn longer<'a>(str1: &'a str, str2: &'a str) -> &'a str {

The meaning is: "Uses of the return value keep both *str1 and *str2 borrowed."

If we accept this as the starting point, we can gloss over thinking about Rust lifetimes for the moment. (We'll discuss how to get the meaning via Rust lifetimes later.)

Let's look at the first example (which does not compile):

{
    let longest;                      // Say the type of this is `&'0 _`.
    let string1 = String::new();
    let str1 = &string1;              // And this is `&'1 _`.
    {
        let string2 = String::new();
        let str2 = &string2;          // And this is `&'2 _`.
        longest = longer(str1, str2); // Line 7
    }                                 // Line 8
    println!("{str1}");               // Line 9
    println!("{longest}");            // Line 10
}                                     // Line 11

The expression &string1 created a borrow of string1, and we want to figure out where that borrow remains active, and similarly for string2. Then we'll see if there are any uses of string1 and string2 that conflict with being borrowed.

&string1 was assigned to str1, so uses of str1 keep string1 borrowed, and similarly for str2/string2. So for example, string1 must still be borrowed through at least line 9, as there is a use of str1 on line 9.

Key to the example is knowing what longest keeps borrowed. In terms of the example, applying the meaning we supplied above means that uses of longest keep both string1 and string2 borrowed.

That means that string1 and string2 stay borrowed through line 10. In particular, string2 is still borrowed on line 8. But on line 8, string2 goes out of scope. Going out of scope conflicts with being borrowed, and this is the source of the borrow check error.


For the second example, we move the use of longest to some place before string2 goes out of scope.

{
    let longest;                      // Say the type of this is `&'0 _`.
    let string1 = String::new();
    let str1 = &string1;              // And this is `&'1 _`.
    {
        let string2 = String::new();
        let str2 = &string2;          // And this is `&'2 _`.
        longest = longer(str1, str2); // Line 7
        println!("{longest}");        // Line 8
    }                                 // Line 9
    println!("{str1}");               // Line 10
}                                     // Line 11

The only thing that keeps string2 borrowed are the explicit uses of str2 on line 7 and of longest on line 8. So string2 need not remain borrowed on line 9, and the conflict from going out of scope goes away.

Explicit uses of str1 and longest keep string1 borrowed through line 10. But string1 does not go out of scope on line 9, and it need not be borrowed on line 11 where it does go out of scope, so there's no conflict there either.

longest does go out of scope on line 11 (and after string1). But references going out of scope does not keep their referents borrowed -- the compiler understands that there is no way for the reference to "observe" its referent when it goes out of scope. It is almost entirely a no-op.

You can think of longest becoming uninitialized when it goes out of scope. If longest itself was borrowed -- if you had a &longest you were trying to keep around -- this may cause a borrow check error. (That's why I said "almost entirely a no-op".)

We had no such nested references in our examples. If there aren't nested references around, most examples that try to illustrate borrow checking by changing where references go out of scope are misleading.[1] You can move the declaration of str2 to the top of main and it won't change the results of either example in any practical way, for instance.


Now let's try to add detail and incorporate Rust lifetimes more explicitly.

The core idea is that the borrow checker

  • Figures out where in the control flow variables[2] are borrowed, and how[3]
  • Then checks every use of every place to see if it conflicts with any borrows[4]

Just like the other replies, we need to distinguish between the lexical or liveness scope of variables -- it's "lifetime" -- and Rust lifetimes (those 'a things). Rust lifetimes approximate the duration of a borrow -- where in the control flow some variable[5] is borrowed.

The main connection between the two concepts is that it is a borrow conflict for a variable to be moved, destructed, or to go out of scope when it is borrowed. That is, going out of scope is a use checked against any potential borrows. If all borrows of a variable end before it goes out of scope, there is no conflict.


Roughly speaking, uses of a Rust lifetime -- uses of values whose type contains a '_ lifetime -- keep borrows alive. In practice this means that uses of a reference keep the referent borrowed, as in the examples.

There is some nuance around when a reference goes out of scope: That is a use of the reference (it conflicts with having a borrow of the reference itself -- like a &&something). But it is not a use of the lifetime -- it does not keep the referent borrowed.

The lifetime in the type of a reference also does not have to correspond to the liveness scope of the reference. Otherwise we couldn't have local variables of type &'static str, for example![6] And longest wouldn't compile either -- both references hold borrows longer than the function body,[7] but we don't return both references.


Function signatures and other lifetime annotations ('b: 'a) convey how uses of lifetimes keep each other alive -- or in practical terms, which borrows the use of a value keeps alive.

As we said before, this signature:

fn longer<'a>(str1: &'a str, str2: &'a str) -> &'a str {

Means "uses of the return value keep both *str1 and *str2 borrowed" in practical terms.

Note that when *str1 and *str2 drop doesn't matter here -- we're just conveying when they remain borrowed. Analysis at the call site will determine if there's a conflict with going out of scope or not.

In the examples, it also doesn't mean "'0, '1, and '2 must all be the same".[8] Why not?

When you pass in your arguments, you're passing in copies of the values.[9] And the types of those copies can have different lifetimes than the original thanks to subtyping, or variance, or however you want to call it. In practical terms, we say the outer lifetime of references can shrink. Or more technically:

  • &'a str can coerce to &'b str if 'a: 'b ("'a outlives 'b")
  • 'a: 'b also means uses of 'b keep 'a active

In the example we can think of the call and assignment to longest as meaning

  • We're calling longest::<'0>
  • Which must mean '1: '0 and '2: '0 (so our arguments can coerce)
  • Which means uses of '0 keep '1 and '2 active
  • Which means uses of longest keep string1 and string2 borrowed

Considering this variation of the function adds some indirection, but we end up with the same conclusion.

fn longer<'a, 'b>(str1: &'a str, str2: &'b str) -> &'a str
where 'b: 'a
{
    str2
}
  • We're calling longest<'0, 'b> where 'b: '0
  • Which must mean '1: '0 and '2: 'b: '0
  • Which means uses of '0 keep '1 and 'b and '2 active
  • Which means uses of longest keep string1 and string2 borrowed

Technically this variation is more general because you can pass in types with different lifetimes. But in practical uses it won't make a difference -- without forcing things by using annotations, there will be nothing in the borrow checking analysis requiring 'b to be any longer than 'a.

(And generally speaking, callers don't want to borrow longer than necessary in order to call such a function.)


I find it unfortunate that the book describes calling longest thusly:

The function signature now tells Rust that for some lifetime 'a, the function takes two parameters, both of which are string slices that live at least as long as lifetime 'a. The function signature also tells Rust that the string slice returned from the function will live at least as long as lifetime 'a. In practice, it means that the lifetime of the reference returned by the longest function is the same as the smaller of the lifetimes of the values referred to by the function arguments.

This implies that the compiler has assigned all the input lifetimes in the calling function ahead of time somehow, and then picks one of those at the call site based on the inputs to fn longest, and therefore the lifetime in longest is determined by the lifetimes in the arguments to fn longest.

But in my mental model,[10] things work exactly the other way around. The uses of the return value (longest) determine where its lifetime must be active (what the lifetime is), and that partially determines what the lifetimes in the inputs to fn longest must be (a superset of the lifetime in longest). Which is why I keep saying:

// Uses of the return value keep both `*str1` and `*str2` borrowed
fn longer<'a>(str1: &'a str, str2: &'a str) -> &'a str {

It is true that 'a has to end before either referent goes out of scope,[11] or you're get a borrow checker error. But the borrow checker does not look at where the referents go out of scope in an attempt to choose some lifetime. It determines the lifetime,[12] and then sees if going out of scope has a conflict.

I suspect that the book's explanation has roots in pre-NLL Rust (i.e. over 8 years ago), when there was a tight connection between how long references borrowed their referents and lexical scopes. (There are sadly many parts of the Nomicon which have also not been updated since those days.)


  1. At best they were relevant before NLL -- i.e. they are over 8 years out of date. ↩︎

  2. and other places, like fields, temporaries, and dereferences ↩︎

  3. exclusively borrowed (&mut) or shared (&) ↩︎

  4. copying a variable doesn't conflict with being shared borrowed but does conflict with being exclusively borrowed, moving a variable conflicts with being borrowed (in any way), etc ↩︎

  5. or other place ↩︎

  6. or Cow<'static, _>, etc ↩︎

  7. inputs lifetimes are always longer than the function body ↩︎

  8. If it did, the second example couldn't compile. ↩︎

  9. If you were passing in &mut _, it would be what we call a reborrow, not a copy. They act similarly in terms of coercion. ↩︎

  10. which is based on the compiler's NLL implementation ↩︎

  11. or gets moved or gets overwritten or has a &mut taken....... ↩︎

  12. and more pertinently the borrow durations (as Rust lifetimes are still an approximation) ↩︎

Very concise :+1:.

Potential alternative phrasing:

        // Because `fn longer`'s output has same lifetime as its inputs,
        // uses of `longest` are also considered uses of `str1` and `str2`.

Thank you. This helps a lot. I have one question, however:

When you said that the call to longer means that '1 and '2 must outlive '0, I don't see how that can be true. '2 lives for a much shorter time, so how can we say it outlives '0?

P.S. I realise that you said the borrowck doesn't actually check for how long borrows live for, so it may not matter if '2 doesn't actually outlive '0, and it is only for the sake of getting to the "uses of 'x keep 'y alive" conclusion.

Please explain!
Thanks.

'0 can be shorter than both '1 and '2. Specifically, '0 can end as soon as println!("{longest}") has finished.

(Also note that lifetimes don't have start times, only end times! The beginning of a borrow is signaled by the reference starting to exist, so lifetimes don’t need to provide any additional constraint — it’s impossible to access a reference before it’s valid, because it doesn’t exist.)

Do you mean, string2 lives for a much shorter time than string1 because of the inner block? If so, remember that Rust lifetimes are about borrow durations, not when values drop.

Once the call is made, longer (with lifetime '0) has to be treated as if it contains the borrows from str1 ('1) and str2 ('2), because either of those cases is possible. So from that point on, wherever borrow '0 is still active, borrows '1 and '2 must be as active well. I.e. '1 and '2 are active for at least everywhere '0 is active. I.e. "'1 and '2 must outlive '0".

If the borrow of string2 ends up lasting longer than string2 itself, that is going to result in a borrow checker error, like in the first example.

An alternative way of thinking about the failing example.

I've presented a way of thinking about the non-compiling example like so: the borrows get computed, those borrows can traverse the destruction of the borrowed object, that results in a conflict that gets reported as a borrow checker error.

Another way of presenting the example could be: the destruction of the borrowed object terminates the borrow, there's an attempt to use the terminated borrow afterwards, that results in a conflict that gets reported as a borrow checker error.

You can think of either presentation as an unsatisfiable constraint problem like @ProgramCrafter said. I prefer the first presentation these days as it's closer to how the implementation works, but either is fine if it helps your mental model.

For the second presentation, you could perhaps think of '2: '0 as "attempts to use borrow '0 also count as attempts to use borrow '2". In any case, bounds like '2: '0 are called outlives bounds, and terminology like "'2 outlives '0" is also common, so you'll have to get used to the relationship in question being called "outlives".

Do you mean, str2 lives for much shorter than str1? If so, that doesn't matter. The borrow duration is not limited by the lexical scope of the reference.

Or if you meant something else, feel free to ask again :slight_smile:.