Skip to content

Latest commit

 

History

History
116 lines (91 loc) · 3.8 KB

lifetimes.md

File metadata and controls

116 lines (91 loc) · 3.8 KB

A C++ comparison 1

You already know that code like this

char* first_letter(std::string& s) {
    return &s[0];
}
...
char* name;
{
    string name_string = "hello";
    name = first_letter(name_string);
} // name_string goes out of scope here
cout << name[0];

is invalid, right? Rust adds extra annotations to let the compiler stop you from doing that sort of thing.

Think of the difference between the following two functions:

const char& first_letter(std::string& s) {
    return s[0];
}

and

const char& first_letter(std::string& s) {
    static const char* letters = "abcdefghijklmnopqrstuvwxyz";
    return letters[s[0] - 'a']; // Pretend I know it starts with a lowercase letter
}

They have identical signatures, but very different behavior with respect to when it's safe to call them. With the first one, you need to make sure you never dereference the returned pointer after reallocating or freeing the string you gave it. With the second one, you can do whatever you want with it, since it's pointing to static memory instead. In Rust, these two functions have different signatures.

fn first_letter<'a>(s: &'a String) -> &'a char

vs

fn first_letter<'a>(s: &'a String) -> &'static char

The 'a is the name of a lifetime, which is some span of code between a constructor and a destructor. The first says "If you give me a reference to a String that is valid for a certain period, I'll give you a reference to a character that is valid for that same period". The second says "I'll give you a reference to a character that is valid for the entire length of the program". If the compiler sees that a caller is violating the contract of the first function signature, that the returned reference is forgotten before the string given to it is destroyed, it will prevent the program from compiling.

##Example 2 1

fn main() { 
    let x = 1;
    let mut y = 2;
    let z = borrow(&x, &y);
    &mut y;
    z;
} // z goes out of scope here

fn borrow<'a, 'b>(u : &'a i32, v : &'a i32) -> &'a i32 { 
  // u and v are immutable borrows, which end when the variables u, v go out of scope
  &u 
} // u and v go out of scope here
  // the borrow of v ends, the ownership of u's borrow is transferred to the return value's binding, so
  // while u goes out of scope, the borrow lives on with another owner

Note that main is perfectly valid, because the reference given to borrow ends after the function is run, so a mutable reference to y can exist after that. I does not compile, though, because the lifetimes in the function signature of borrow claim "The lifetimes of the borrows for u and v are the same as the lifetime of the return value of that function", and the return value's ownership is transferred to z. So the signature claims to the compiler that the borrow &y in the call to borrow lives until main finishes, which conflicts with the mutable binding &mut y;.

In conclusion, the code is perfectly valid, the compiler just can't see this fact, because we made wrong claims about lifetimes here.

We can fix this by changing v: &'a i32 to v: &'b i32, that is to

fn main() { 
    let x = 1;
    let mut y = 2;
    let z = borrow(&x, &y);
    &mut y;
    z;
} 

fn borrow<'a, 'b>(u : &'a i32, v : &'b i32) -> &'a i32 { 
  &u 
} 

The lifetime claims now are "The first argument's borrow lives as long as the return value, the second one has a lifetime that is independent from that". Now the compiler can verify that the code is correct, and happily compiles for us.

References

1 https://www.reddit.com/r/rust/comments/587ecn/looking_to_possibly_learn_rust_i_come_from_a/d8y9gxl

2 rust-lang/rust#36997 (comment)