In C++ (please correct me if wrong), a temporary bound via constant reference is supposed to outlive the expression it is bound to. I assumed the same was true in Rust, but I get two different behaviors in two different cases.
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:
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.
Is this intended and if so then what is the rationale for the difference in behavior in the two cases?
I am using Rust 1.12.1.
Temporaries are dropped at the end of the statement, just like in C++. However, IIRC, the order of destruction in Rust is unspecified (we'll see the consequences of this below), though the current implementation seems to simply drop values in reverse order of construction.
There's a big difference between
let _ = x;
andlet _b = x;
._
isn't an identifier in Rust: it's a wildcard pattern. Since this pattern doesn't find any variables, the final value is effectively dropped at the end of the statement.On the other hand,
_b
is an identifier, so the value is bound to a variable with that name, which extends its lifetime until the end of the function. However, theA
instance is still a temporary, so it will be dropped at the end of the statement (and I believe C++ would do the same). Since the end of the statement comes before the end of the function, theA
instance is dropped first, and theB
instance is dropped second.To make this clearer, let's add another statement in
main
:This produces the following output:
So far so good. Now let's try with
let _b
; the output is:As we can see,
Drop B
is printed afterEnd of main.
. This demonstrates that theB
instance is alive until the end of the function, explaining the different destruction order.Now, let's see what happens if we modify
B
to take a borrowed pointer with a lifetime instead of a raw pointer. Actually, let's go a step further and remove theDrop
implementations for a moment:This compiles fine. Behind the scenes, Rust assigns the same lifetime to both the
A
instance and theB
instance (i.e. if we took a reference to theB
instance, its type would be&'a B<'a>
where both'a
are the exact same lifetime). When two values have the same lifetime, then necessarily we need to drop one of them before the other, and as mentioned above, the order is unspecified. What happens if we add back theDrop
implementations?Now we're getting a compiler error:
Since both the
A
instance and theB
instance have been assigned the same lifetime, Rust cannot reason about the destruction order of these objects. The error comes from the fact that Rust refuses to instantiateB<'a>
with the lifetime of the object itself whenB<'a>
implementsDrop
(this rule was added as the result of RFC 769 before Rust 1.0). If it was allowed,drop
would be able to access values that have already been dropped! However, ifB<'a>
doesn't implementDrop
, then it's allowed, because we know that no code will try to accessB
's fields when the struct is dropped.Raw pointers themselves do not carry any sort of lifetime so the compiler might do something like this:
Example:
*const A
in it)Let's check out the MIR:
As we can see
drop(_1)
is indeed called beforedrop(_6)
as presumed, thus you get the output above.In this example B is bound to a binding
The corresponding MIR:
As we can see
drop(_6)
does get called beforedrop(_1)
so we get the behavior you have seen.