"Borrowed value does not live long enough" with destructor

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;
}
1 Like

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.

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.

1 Like

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

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.

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);
    }
}

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.)

2 Likes

/cc @nikomatsakis, @ubsan

2 Likes

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.

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?

1 Like

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 ?

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.

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?

How would the ordering be determined?

Rust already defines the order of operations.

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.

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!)

(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.)

It's not done yet -- see Settling execution order for =, +=

Here's a more direct adaptation of that example, to show that it works even if the lifetimes aren't equal:

struct A;

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

fn main() {
    let outer = A;
    {
        let inner = A;
        borrow_1(&outer, &inner);
        borrow_2(&inner, &outer);
    }
}

It works because 'b: 'a only requires that 'b be at least as long as 'a; if they're equal, that's acceptable. So when checking main, the borrow checker simply lets both 'a and 'b be the smaller of the two lifetimes (using separate instances of 'a and 'b for each of the calls, of course).

1 Like