Lifetimes and no memory corruption guarantees

Is it possible to access "wrong" (wrong in the sense that the reference already doesn't exist) reference if I make an error as a programmer and will confuse lifetimes?

With lifetimes let's say in function:

fn some<'a,'b>(a_slice: &'a str, b_slice:&'b str, anoter_a: &'a str)-> &'a str
{
a_slice
}

If I as a programmer make a mistake and instead of marking a_slice with 'b I've marked it with 'a but reality is that it should be b, will the borrow checker let this code pass but the program will panic?

It's impossible to use a reference when its target doesn't exist anymore. The only time you'd need to make sure not to mess up your lifetimes for your program not to violate memory safety is when you are using unsafe code blocks. In your example you don't use unsafe, thus if you get the lifetimes wrong the code won't compile anymore.

I need to clarify my last statement a bit as it sounds like “wrong” lifetimes are always compiler errors and the Rust compiler always knows which lifetime annotations you need to write. To the point that people are wondering why you need to annotate lifetimes at all and why the compiler is annoyingly hesitating in telling you the correct lifetimes even if it ought to already know them.

The compiler doesn’t already know the correct lifetimes for every function in advance and it is still possible to get lifetime annotations wrong (in unsafe-free code), but never in a way that compromises memory safety. They can be wrong in the sense that they are not general enough, the consequence would be that your API becomes hard or impossible to use. It is not any memory-corrupting runtime-errors you need to worry about when writing lifetime annotations but the compile-time errors that users of your code might get. When you are your own user, this means that the compiler could complain about the code near the use site of a function while the real (and actually fixable) error was made in the lifetime annotations of the definition of that function.

In your example code, you could just try to mess up the lifetimes yourself and see when you’ll get a compile error. No need to ask anybody about what the compiler will tell you when you can ask the compiler yourself.

3 Likes

Nope.

Lifetimes have no effect whatsoever on the (runtime) behavior of your code. They are a contract, a promise you make when you're writing a function to uphold certain relationships. The borrow checker just determines whether you're holding up your end of the deal or not. There are basically three ways to get lifetimes "wrong":

  1. You promised too much and failed to deliver. You get a compile error inside some. (This is what happens if you change a_slice to type &'b str in your example)
  2. You promised too little. You delivered on it, but the contract was useless because the caller needs to rely on something you didn't specify. You get a compile error at the call site of some. This mistake can be harder to fix because it can cause errors in multiple other places, and it's not always obvious that the problem is actually in some.
  3. You promised too little, but (in this case) it didn't matter because the caller wasn't relying on the actual behavior of some, so the weak promise was sufficient. Everything compiles and works. Next week you write another function that tries to use some, but wants to rely on the promise you meant to make, and you get a compile error in that function instead. Fortunately, because you underpromised in some, you can fix this without breaking existing code.

#1 is the easiest to deal with. #2 and #3 are hazards you can fall into when you follow the compiler guidelines too literally, which is why you need to understand the lifetimes in your code and not just always take the compiler's advice.

1 Like

OK guys, thanks for the replies.
I'm beginning to get the feel for Rust.
Thanks for your help.

This will be best seen with an example. Let's take the one the OP provided, a bit simplified for clarity:

  • Aside: the above is a correct function declaration (that thus compiles), in that the returned object, a_slice, does indeed have a 'a lifetime. If we had annotated the return value with 'b instead,

    - ) -> &'a str
    + ) -> &'b str
      {
           a_slice
      }
    

    then the program would not have compiled anymore.

But let's indeed consider this other mistake the OP suggested: using 'a for the lifetime of b_slice too:

- fn some<'a, 'b> (
+ fn some<'a,   > (
      a_slice: &'a str,
-     b_slice: &'b str,
+     b_slice: &'a str,
  ) -> &'a str
  {
      a_slice
  }

This compiles fine, so no unsoundness here. And yet the intuition that this may be a (minor) mistake is correct. Indeed, consider the following usage:

fn example ()
  -> &'static str
{
    let a_slice: &'static str = "a"; // &'static
    let b_slice: &'_ str = &*String::from("b"); // &'__local__
    some(a_slice, b_slice)
}

With the initial <'a, 'b> signature, the second parameter is completely ignored / plays no role whatsoever, so this example() function compiles fine.

But when b_slice is restricted to use the same lifetime as a_slice, or, in other words, when a_slice is restricted to use the same lifetime as b_slice,

since the lifetime of b_slice is '__local__ ≠ 'static, then 'a ≠ 'static and thus the returned value some(a_slice, b_slice) is not 'static either,

causing a compilation error.


TL,DR: by mis-annotating the lifetime parameters of our function, we have made it impossible to call it in some ways. No unsoundness there, just ergonomics / overly restricted API annoyance.

1 Like

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.