[Cover image credit: https://pixabay.com/users/engin_akyurt-3656355]
The build up
Just like other enthusiastic Rust newbies, I am trying to trek through the difficult terrain of Rust's Borrow-checker, with excitement, mixed with trepidation. Many times, I fall foul of the compiler, because I am careless or I lose sight of the obvious. But, some other times, compiler's reprimands baffle me, to no end.
One such case is of compiler flagging: temporary values getting freed at the end of the statement!
A method-call expression like:
a.b().c().d;
is so common in a program that one tends to write it, almost by reflex. Yet, some times the compiler derails the line of thought, unexpectedly, pointing out release of a temporary variable, that one hasn't even mentioned.
I had decided to understand the whys and the wherefores of this particular problem, to reasonably good extent. At least, I should know how to avoid those. This is a digest of that exploration.
Simulate the error
An Employee
has an id ( i32
). It implements a get_details()
method.
This method, intentionally, returns a new Details
Object (the reason behind this, will be clearer later). The object initializes the name
field with a String constant, which it owns!
#[derive(Debug)]
struct Employee {
id: i32
}
#[derive(Debug)]
struct Details {
emp_id: i32,
name: String
}
impl Employee {
pub fn get_details(&self) -> Details {
Details { emp_id: self.id, name: String::from("Nirmalya") }
}
}
impl Details {
pub fn get_name( &self ) -> String {
self.name
}
}
fn main() {
let e = Employee { id: 10 };
let n = e.get_details().get_name();
println!("Employee name: {}", n);
}
The compiler frowns, expectedly ...
error[E0507]: cannot move out of `self.name` which is behind a shared reference
--> src/main.rs:31:9
|
31 | self.name
| ^^^^^^^^^ move occurs because `self.name` has type `String`, which does not implement the `Copy` trait
We are not allowed to 'move' the name here; the &self
parameter is prohibiting it. The move requires the Details
to be modified. The get_details()
method taken in an immutable reference: &self
. After the method returns, self.name
cannot remain uninitialized.
We cannot move but we can possibly share a reference then?
impl Details {
pub fn get_name( &self ) -> &String { // String is replaced with &String
&self.name
}
}
The compiler finds the temporary which is freed prematurely, this time.
error[E0716]: temporary value dropped while borrowed
--> src/main.rs:41:13
|
41 | let n = e.get_details().get_name();
| ^^^^^^^^^^^^^^^ - temporary value is freed at the end of this statement
| |
| creates a temporary value which is freed while still in use
42 |
43 | println!("Employee name: {}", n);
| - borrow later used here
|
help: consider using a `let` binding to create a longer lived value
|
41 ~ let binding = e.get_details();
42 ~ let n = binding.get_name();
|
For more information about this error, try `rustc --explain E0716`.
So, changing the body and return type of Details::get_details()
is causing an otherwise harmelss method-call expression to fail.
But why? Let's find out more about this.
Elaboration
A binding comes into existence with the let
statement: e
is bound to and thereby is owning an Employee
struct(ure).
let e = Employee { id: 10 };
We are trying get an access to the Employee's name. This name is not a part of Employee
but of a different object of type Details
.
let n = e.get_details().get_name();
What get_details()
produces is a Details
object. We are calling get_name()
on it. What's there to complain about?
Well, the get_name()
is returning a reference to a member variable of Details
. For this reference to remain valid (pointing to some place which is initialized with some known value), Details
has to keep existing. But, till when? The answer lies in establishing its scope.
Expresssions and Scopes
This is a method-call expression:
e.get_details().get_name();
get_details()
is a direct method on the type Employee
. Here, e
is called the receiver of method-call.
get_name()
is a direct method on the type Details
. But, where is the receiver of this method-call? Taking a closer look, we find that an unnamed object of type Details
has been made available - temporarily - so that we can call the method. Our intention is to be able to get hold of what get_name()
returns. We don't really care about this faceless helper object, though.
The key understanding here, is that get_name()
returns a reference. It refers to a field of Details
object. If and when this (temporary) Details
object ceases to exist (or Drop-ped, if you like), the reference is invalidated.
By the rule of scope of such temporaries (more here), they can live till the end of the statement that contains the call that produces it. Interpeting this vis-a-vis our example, by the time the whole expression is evaluated and the statement ends, the faceless Details
object has gone out of existence. Therefore, it is illegal to hold a reference to a field that it owns. So, n
cannot be assigned to.
This explains the complaint from the compiler, shown above.
Going a little further
One way, not to let the faceless Details
object go, is to bind it to a memory location.
fn main() {
let e = Employee { id: 10 };
let intermediate_d = e.get_details(); // <--- Binding!
let n = intermediate_d.get_name(); // <--- Method call expression
println!("Employee name: {}", n);
}
In this case, the compiler is happy and waves us ahead. But, why exactly?
Because we have bound the hitherto faceless Details
object to intermediate_d
, its scope has now been extended to the end of the block it belongs to (in this case, the end of the body of function, i.e.,main()
). Thus, getting hold of the name
inside it, is not illegal anymore.
This was what the ever helpful Rust compiler suggested in the error message, shown earlier! 👌 🌟
Interestingly, the following code is flagged as alright, by the compiler.
fn main() {
let e = Employee { id: 10 };
let n = e.get_details().get_name().len(); // <-- expression produces a value.
println!("Employee name: {} chars long", n);
}
In this case, the length of the name is what we hold in n
. The faceless Details
object, goes away by the end of the statement, but it doesn't matter. We are not holding any reference to Details.name
anyway.
Even this works, but for a slightly different reason:
impl Details {
pub fn get_name( &self ) -> String {
self.name.to_owned() // <-- Not a reference, but a cloned value, unassociated with `self.name`
}
}
// ........
fn main() {
let e = Employee { id: 10 };
let n = e.get_details().get_name();
println!("Employee name: {}", n);
}
Because get_name
returns a value that is owned by - and not referred to by - the caller, the non-existence of the faceless Details
object is immaterial here. n
is bound to a fresh replica of what was held by Details.name
. Its scope extends till the end of the block; in this case, the end of body of function, i.e., main()
.
Main Takeaways
- Method chains are Method-call expressions. Like every expression, they evaluate to a value.
- Every dot ( . ) operation is a method/function, which operates on an object, called the 'Receiver' according to the Book (rust book). The receiver's type must have that method defined on itself.
- In some cases, in order to determine the intended receiver, the compiler follows the references along the expression and produces temporary objects as receivers. The scope of these temporary objects extends up to the end of the statement they are contained in (not exactly, but let's continue till subsequent articles on this topic). Therefore, the compiler considers any attempt to access these temporary objects through references, after the statement, as illegal.
- The simplest way to avoid this, is to declare local variables to bind to these temporary objects. The helpful compiler even hints at this, along with the error message.
Acknowledgements
In my quest to understand the behaviour of the compiler in this case, I have gone through a number of articles / blogs / explanations. I will be unfair and utterly discourteous on my part, if I don't mention at least, some of them. This blog stands on the shoulders of those authors and elaborators:
- https://fasterthanli.me/articles/a-rust-match-made-in-hell from one of the most readable and elucidative take on all things Rust, by Amos ( @fasterthanlime ).
- A brief yet illuminative answer by Sven Marnach on Stackoverflow.
- An useful question on Stackoverflow by Bosh and fantastic answer by Jim Blandy. This answer has brought forth that Aha moment for me but the discussion trail itself is very helpful (thanks everyone else, in that ticket).
- The chapter on Destruction in the Rust wiki and a bit of my own expeimentation
- An old but quite useful explanation of scoping; recommended!
- Of course, the relevant pages of The Rust Wiki, an excellent aid for a newbie like me.
- My companion book by Jim Blandy (again!), Jason Orendorff, Leonora Tindall
Top comments (0)