gdritter repos documents / master posts / some-rust-errors.md
master

Tree @master (Download .tar.gz)

some-rust-errors.md @masterview markup · raw · history · blame

I was an early evangelist of the [Rust language] at my office, having followed it development for a few years, but I still haven't written any large programs in it. So I don't yet have a really strong opinion on the language in practice.

However, [a coworker] has started writing a program in Rust, and it has given me the opportunity to better understand the language so I can answer his questions. Whenever something would go wrong, he would sent me the code, or a representative snippet, and demand that I explain why it wasn't working. Some of these questions were actually quite difficult, and in at least one case, I was briefly convinced that I had found a compiler bug.

Because these are some tricky interactions with the language, I wanted to write this up as a second-hand experience report, describing the problems that came up and explaining why they were problems.1

Problem One: Determining Temporary Lifetimes

Rust has a simple rule for determining the lifetimes of temporary values. A temporary value is any value which is not directly bound to a name, but is created somewhere in your program. For example, the return value of a function that is not directly assigned is a temporary, or a struct created for the express purpose of taking a reference to it.

Rust's rule is that, generally, temporaries live only for the statement in which they are created. For illustration's sake, let's create an empty struct with a noisy constructor and destructor, so we see when values are being created or destroyed:

struct Thing;

impl Thing {
    fn new() -> Thing {
        println!("Created Thing");
        Thing
    }
}

impl Drop for Thing {
    fn drop(&mut self) {
        println!("Destroyed Thing");
    }
}

If I create something and don't bind it to a name, then it'll get destroyed before the next line executes:

fn main() {
    Thing::new();
    println!("fin.");
}
/* This prints:
> Created Thing
> Destroyed Thing
> fin.
*/

The exception to this rule happens if I bind a reference to the thing. The thing itself is still a temporary because, even though we can access it, there's nothing in scope that has ownership over it, we can't pass the ownership elsewhere or force it to drop or anything. However, if we have a reference to it (or some part of it), then it will live as long as the reference does. For example, because of the reference here, this temporary will live longer than before:

fn main() {
    let r = &Thing::new();
    println!("fin.");
}
/* This prints:
> Created Thing
> fin.
> Destroyed Thing
*/

This heuristic only fires of you're directly binding the temporary to a reference in that expression. This came up for my coworker because he had some initialization logic that he thought would be better served by pushing it into a function, so he wrote the equivalent of

fn mk_thing_ref(thing: &Thing) -> &Thing {
    thing
}

fn main() {
    let r = mk_thing_ref(&Thing::new());
    println!("fin.");
}

This refactor means that the temporary returned by Thing::new() is no longer directly being bound to a reference, and therefore the rule no longer applies: the result of Thing::new() will die before the next line. This is a problem, because r continues to exist after that line, which means this program is rejected by the Rust compiler.

temp.rs:21:21: 21:33 error: borrowed value does not live long enough
temp.rs:21     let r = mk_ref(&Thing::new());
                               ^~~~~~~~~~~~
temp.rs:21:35: 23:2 note: reference must be valid for the block suffix following statement 0 at 21:34...
temp.rs:21     let r = mk_ref(&Thing::new());
temp.rs:22     println!("fin.");
temp.rs:23 }
temp.rs:21:5: 21:35 note: ...but borrowed value is only valid for the statement at 21:4
temp.rs:21     let r = mk_ref(&Thing::new());
               ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
temp.rs:21:5: 21:35 help: consider using a `let` binding to increase its lifetime
temp.rs:21     let r = mk_ref(&Thing::new());
               ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
error: aborting due to previous error

In this case, the error is clearer, but in my coworker's case, he was trying to encapsulate much more elaborate initialization logic that abstracted away gritty details, and was confused that what should be an equivalent refactor no longer worked. It didn't help that he was initializing something with closures, making him believe that it was the closures that were at fault.

Problem Two: Lifetimes of Trait Objects

A different lifetime problem came up elsewhere:


  1. Some of these I suspect will be alleviated by better error messages, but are probably niche enough that improving these errors hasn't been a high priority.