"Borrowed value does not live long enough" with destructor


#1

Why doesn’t this work?

struct A;
struct B<'a>(&'a A);

impl<'a> Drop for B<'a> {
    fn drop(&mut self) {}
}

fn main() {
    B(&A);
}

Rust complains:

error: borrowed value does not live long enough
 --> ab.rs:9:8
  |
9 |     B(&A);
  |        ^ - temporary value dropped before borrower
  |        |
  |        temporary value created here
  |
  = note: values in a scope are dropped in the opposite order they are created
  = note: consider using a `let` binding to increase its lifetime

Using a let binding to increase the lifetime of that A value does fix it.

But why is there a problem in the first place? Shouldn’t the A value be created before, and thus outlive, the B value?


Understanding when a Drop implementation can extend a borrow
#2

In C++, the answer would be that the scope of A is only within the parentheses, so it is created just before B is created and passed into B, but then at the end of the call to create B, A goes out of scope and is no longer valid to use. This, of course, is before B goes out of scope at the end of the function. So if anything inside B tried to use its reference to A, it would actually be using stack space that could have been reused by some other variable. Moving A out to a let on a separate line gives it the same scope as B, so it all works out.

I’m a Rust newbie, but I think Rust has the same scoping rules here, and that’s why it flags this as an error.

I’ve encountered very similar bugs in the past when writing C++, where the compiler would reuse stack space from an object that had been created and gone out of scope, but other objects still held a reference to those objects. The tricky thing there is that they often don’t bite you right away, but then you change something unrelated in the function later, suddenly it blows up. So it’s nice that Rust catches this.

EDIT: I realize my description above may be a bit obtuse. Let me know if this doesn’t make sense and I’ll try to explain it in a different way.


#3

I think in C++, the temporary lasts to the end of the complete expression. Here’s a C++ example; the output shows A being created before, and destroyed after, B.

#include <iostream>

using namespace std;

struct A {
    A() { cout << "A()\n"; }
    ~A() { cout << "~A()\n"; }
};

struct B {
    const A* a;
    B(const A& a) : a(&a) { cout << "B()\n"; }
    ~B() { cout << "~B()\n"; }
};


int main() {
    (void) B(A());
    cout << "done\n";
    return 0;
}

#4

Ah, so it does. As usual, the best way to verify a behavior is to actually try it :slight_smile: Thanks for the correction.

A similar experiment using println! reveals that B is dropped before the end of the function as well, which leaves us back with your original question. I only have idle speculation at this point, but will be interested to see what the experts have to say.


#5

In Rust, the reference says:

When an rvalue is used in an lvalue context, a temporary un-named lvalue is created and used instead. The lifetime of temporary values is typically the innermost enclosing statement; the tail expression of a block is considered part of the statement that encloses the block.

This doesn’t seem to match what the compiler’s saying in its error message rejecting the original example.

(I understand that the Rust Reference isn’t authoritative, but I wasn’t able to find a discussion of this in The Book, and the compiler’s behavior doesn’t make sense to me here.)

In C++14, the relevant chapter and verse is §12.2/3 Temporary Objects [class.temporary]:

Temporary objects are destroyed as the last step in evaluating the full-expression (1.9) that (lexically) contains the point where they were created.

§1.9/10 Program execution [intro.execution] explains:

A full-expression is an expression that is not a subexpression of another expression.

So even in C++, the temporary should last to the end of the call.


#6

My understanding here is that the lifetime of A and B must be the same, hence the problem.


#7

That would explain why it works fine if I remove the impl Drop for B.

But why do they have to be the same?

  • the A value has to outlive the borrowed reference to it
  • the B value must not outlive the borrowed reference to A

So far it’s solvable. There must be another constraint I don’t see.


#8

The error message of the stable compiler is actually a bit clearer here:

rustc 1.12.0 (3191fbae9 2016-09-23)
error: borrowed value does not live long enough
 --> <anon>:9:8
  |
9 |     B(&A);
  |        ^ does not live long enough
  |
note: reference must be valid for the destruction scope surrounding statement at 9:4...
 --> <anon>:9:5
  |
9 |     B(&A);
  |     ^^^^^^
note: ...but borrowed value is only valid for the statement at 9:4
 --> <anon>:9:5
  |
9 |     B(&A);
  |     ^^^^^^
help: consider using a `let` binding to increase its lifetime
 --> <anon>:9:5
  |
9 |     B(&A);
  |     ^^^^^^

error: aborting due to previous error

It seems that the drop is called in an implicit scope surrounding the actual statement, like:

fn main() {
    { // implicit scope for `drop`s
        let b = { // implicit scope of the original statement
            B(&A)
        };
        drop(&mut b);
    }
}

#9

Okay, but why doesn’t A’s lifetime enclose B’s? It’s created first, so it should be destructed last, is my understanding.

(Jason and I are trying to understand the principle here. The workaround is fine, but we don’t understand why it’s necessary.)


#10

/cc @nikomatsakis, @ubsan


#11

Okay, @bluss and @talchas kindly explained this to me on IRC.

The problem is in the granularity of the drop-checking rules, which require A to strictly outlive B. From the point of view of that code, A and B have the same lifetime, as if one had written:

struct A;
struct B<'a>(&'a A);

impl<'a> Drop for B<'a> {
    fn drop(&mut self) {}
}

fn main() {
    let (a, b);
    a = A;
    b = B(&a);
}

The reason the presence of the Drop impl matters is that, when it’s only references involved, it’s permissible for reference and referent to have identical lifetimes: the reference can’t be used unsafely. Introducing a Drop impl to the situation requires the referent to strictly outlive the reference, to ensure there is a clear order in which to run drop methods.


#12

OK, but I already understood that part.

What I don’t get is why the two temporaries have the same lifetime! Where does that constraint come from? What rule? And is the rule necessary for soundness?


#13

There is some inconsistency (I’ve never looked at Rust or C++ Ref/Std deeply so I might be wrong).

In C++ a temp bound by a const ref is supposed to outlive the site of binding and the object being bound to. In rust this does not seem to be the case. Just need to bypass the lifetime check to know what’s happening - so using pointers.

Consider:

struct A;
impl Drop for A { fn drop(&mut self) { println!("Drop A.") } }

struct B(*const A);
impl Drop for B { fn drop(&mut self) { println!("Drop B.") } }

fn main() {
    let _ = B(&A as *const A); // B is destroyed after this expression itself.
}

The output is:

Drop B.
Drop A.

This is what you would expect. But now if you do:

struct A;
impl Drop for A { fn drop(&mut self) { println!("Drop A.") } }

struct B(*const A);
impl Drop for B { fn drop(&mut self) { println!("Drop B.") } }

fn main() {
    let _b = B(&A as *const A); // _b will be dropped when scope exits main()
}

The output is:

Drop A.
Drop B.

This is not what i expected - but if this is intended then to be on the safe side i think the compiler disallows your code in OP.

Anyway, now i’ve a question - is that intended behaviour and if so why is there a difference ?


#14

TBH, the second one is what I’d expect.
A is a temporary, that only lives for the statement while B is a named variable that lives until the end of main.
The first variant is probably a special case and B is regarded as a temporary as well, because _ is not really a named variable.

Anyway, B takes a pointer (not a reference) to A, so A is not borrowed and the lifetimes of A and B are not related at all. That’s why I’d say that the example is not relevant to the original problem.


#15

I guess that all temporaries in the same expression are given the same lifetime.
Otherwise the compiler would have to determine a strict ordering between all temporaries.

I image that this would also lead to more (but different) restrictions. How would the ordering be determined? Left to right? Inside to outside? Or dynamically to determine the most “liberal” ordering?


#16

How would the ordering be determined?

Rust already defines the order of operations.


#17

And still the following compiles:

struct A;

fn borrow_1<'a, 'b: 'a>(_: &'a A ,_: &'b A) { }
fn borrow_2<'b, 'a: 'b>(_: &'a A ,_: &'b A) { }

fn main() {
    borrow_1(&A, &A);
    borrow_2(&A, &A);
}

It wouldn’t if they were strictly ordered.


#18

I think that would still work, for the same reason that this is acceptable:

fn f<'a>(_a: &'a mut i32, _b: &'a mut i32) {}

fn g<'a, 'b>(a: &'a mut i32, b: &'b mut i32) {
    f(a, b);
}

g can call f even though f requires arguments with matching lifetimes, because the arguments are reborrowed (with a shorter, common lifetime) at the f(a, b) call site.

(Update: Actually, even if this were not the case, the reference lifetime of &A need not exactly match the lifetime of the A temporary, IIUC, so it might still work!)


#19

(Also, it seems reasonable for temporaries without destructors to be grouped together and given the same lifetime, just like bindings for values without destructors that are declared in the same pattern.)


#20

It’s not done yet – see Settling execution order for =, +=