Rust Notes on Temporary values (usage of Mutex) - 1

nsengupta

Nirmalya Sengupta

Posted on June 14, 2023

Rust Notes on Temporary values (usage of Mutex) - 1

[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;
Enter fullscreen mode Exit fullscreen mode

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

}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
    }
}
Enter fullscreen mode Exit fullscreen mode

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`.
Enter fullscreen mode Exit fullscreen mode

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 };
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

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);     
}
Enter fullscreen mode Exit fullscreen mode

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);     
}
Enter fullscreen mode Exit fullscreen mode

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);     
}
Enter fullscreen mode Exit fullscreen mode

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
💖 💪 🙅 🚩
nsengupta
Nirmalya Sengupta

Posted on June 14, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related

December Surely Looks Busy!
opensource December Surely Looks Busy!

November 29, 2024

December Surely Looks Busy!
opensource December Surely Looks Busy!

November 29, 2024

Daemons on macOS with Rust
undefined Daemons on macOS with Rust

November 29, 2024