Lifetime and generic in plain English

Hello Quinedot,

Thank you very much for your responses ! It helped me a lot.

You point out an interesting point : usually (as far as I've noticed), we talk about the lexical scope (of a variable). When I talk about "scope" with students, they immediately think "lexical."

Please note that this may be a French particularity.

Thus, in order to avoid misunderstanding, it is best to use this term to refer to a lexical scope only (because we can think of other types of scopes, of course), and to always use it in conjunction with the adjective "lexical."

But lifetimes haven't been restricted to lexical scopes since 2018.

Yes, we agree. The term "lexical scope" applies to a variable, while the "lifetime" applies to a value. And it is true that a value may very well outlive a variable that temporarily identifies it. That's typically the case, in C programming, when (from within a function) you return a pointer to a memory allocation buffer. The value (the content of the buffer) still exists outside of the function, while the (local) variable within the function that referred to the buffer vanished.

The liveness of a borrower cannot outlive the borrow lifetime. I.e., the borrower must not be used (or usable) outside of the borrow lifetime. But that liveness region might not correspond to a lexical / drop scope.

Given the definition "We say that a variable is live (alive ?) if the current value that it holds may be used later" : that seems perfectly logic.

Is the concept of "liveness" used within a C compiler ?

I mean : in C programming, you can (by mistake) use a variable, while the value it "held" (I used the past) is not usable anymore. The C compiler does not check whether or not it is fine to use the value held by a variable. How could the C compiler could find out that a value is not usable anymore, anyway ? It seems impossible.

The scope of a &T or &mut T variable never really matters. The scope of a struct containing such a borrower can matter, if it has a destructor -- because that destructor will run at the end of the scope.

That's clear.

The scope of borrowed -- the T variable you're creating a &T to -- is an upper limit on the borrow lifetime.

That's clear.

Rust does have "raw pointers" in addition to references.

I haven't seen "raw pointers" yet. I imagine that "raw pointers" are necessary, in other things, to interface with C binaries. But one step at a time. I'll see that later.

Thank again !

In Rust, normally, and in the sense used in "non-lexical lifetimes", "lifetime" applies not to a value but to a borrow. A borrow may be associated with many values, or none in particular.

C doesn't have the concept of borrowing (well, on a language level - borrowing is actually a very important practical concept in real-world C code). So, applying the senses of liveness and lifetime that we use to describe Rust's borrow checker to what a C compiler does is incoherent.

1 Like

Hello Trentj,

When you say:

I guess that you mean:

    let mut v1: u8 = 10;
    let mut v2: u8 = 20

    let mut borrow: &mut u8;
    borrow = &mut v1;
    borrow = &mut v2;

You mean that a "borrow" (in the sense : "a variable that is a reference") may refer to more than one value, successively, not simultaneously.

Thus, to sum up:

If I refer to the (very interesting link given by Quinedot) :

  • "lifetime" applies to a borrow (that is, a reference that refers to a value).
  • (lexical) "scope" applies to a variable (that owns a value).
  • "liveness" applies to a variable.

For the previously cited document :

And here :

Do we agree on the following ?

  • "lifetime" applies to a borrow (that is, a reference that refers to a value).
  • (lexical) "scope" applies to a variable (that owns a value). A variable EXISTS or not.
  • "liveness" applies to a variable. A variable is DEAD or ALIVE.

A variable may exist as being DEAD (x)or ALIVE (the liveness is a state).

But don't tell me that a variable can be dead and alive at the same time... as the Schrodinger's cat :joy: This state may exist in quantic programming, though.

Thanks !

This doesn’t line up with my mental model, which might be a bit different from others’:

When you take a reference of a type T, two things happen:

  • T is borrowed for some validity region (’a for sake of discussion) starting at that point, and
  • A value of type &’a T is created that points to the original object.

The borrow checker ensures that the reference &’a T doesn’t escape the borrow’s validity region ’a, but the reference can be dropped before the borrow ends. You can also get multiple values associated with the same borrow through methods like split_at_mut, which destroys the original reference but also returns several new values tied to the original borrow.


The most important point here is that a “borrow” is its own thing that doesn’t quite line up with any of the concrete things that you can point to in the program text:

  • It’s not a lifetime annotation; that’s a generic parameter that can represent any number of borrows over the course of a program
  • It’s not a reference, but taking a reference is the most common way to borrow something
  • It’s not a variable, because it doesn’t have any runtime representation

Instead, it’s a heuristic for thinking about your code that lines up well with the analysis that the borrow checker uses to verify the memory usage in your program.

3 Likes

A borrow is not a variable, a reference, or a value. A borrow is a region of the control flow graph that the borrow checker uses to prove that uses of a particular reference or variable are allowed. A borrow in the sense used by Rust does not correspond to any concept in C, C++, or any other mainstream programming language; none of them have compile time borrow checking.

In the following code,

let x = 4;
let r;
{
    let y = 10;
    r = if x > y { &x } else { &y };
}

r is a variable that holds a reference to y. But that reference is considered by the compiler to borrow both x and y. So yes, a borrow may be associated simultaneously with multiple values ("loans" in the Stacked Borrows terminology). r doesn't, of course, refer to both x and y; it only refers to y. But that is a fact about the runtime behavior of the code, and borrows are facts about the compile time analysis of the code.

3 Likes

Hello 2e71828 and Trentj,

Thank you very much for your explanations. That's much clearer.

The key that triggered my understanding is (as you both say) : the "borrow" does not "materialize" itself in the source code. Thus, one should not try to find "something" in the code that "is" a "borrow": there is no such thing in the code. A "borrow" "exists" within the representation of the code built by the compiler.

If you did not tell me, I'd never find out... at least not soon. I've always used compilers without looking at how the thing actually works.

Now, I need to "digest" that and to reformulate everything.

Again, thanks a lot !

1 Like

I let things settle down a bit. Here is a summed.

concept applies to remark
lexical scope variable a lexical scope generally corresponds to some block (or, more specifically, a suffix of a block that stretches from the let until the end of the enclosing block).
liveness variable liveness(variable) can be LIVE (owns a value) or DEAD (does not own a value anymore).
move (verb or noun) value to move a value <=> to transfer of a property title (on a value) from one variable to another.
to borrow (the verb, as opposed to the noun) value to borrow a value <=> to temporarily transfer a property title (on a value) from a variable to another.
a borrow (the noun, as opposed to the verb) does not materialize in the code. This noun is related to the compiler representation (of the code).
lifetime a borrow a borrow may be associated with many values, or none in particular (Trentj).

A simple illustration :

use rand::random;

/// The return value is "entangled" with the parameters' ones.
/// If one of the value owned by the parameters (`p1` or `p2`) moves, then the "entanglement"
/// is broken => and thus the return value is not usable anymore.
fn entangle_values<'a>(p1: &'a Box<u8>, p2: &'a Box<u8>) -> &'a Box<u8> {
    let r: u8 = random();
    if r > 128 { p1 } else { p2 }
}

fn move_value<T>(_v: T) { }

fn use_value<T>(_v: T) { }

fn borrow_value<T>(_v: &T) { }

/// Liveliness vs lexical scope.
fn test1() {
    let mut variable: Box<u8> = Box::new(5); // `liveness(variable)` is "LIVE".
    move_value(variable); // `liveness(variable)` is "DEAD".
    // `variable` cannot be used as an operand since it does not own any value anymore.
    // But `variable` is still within the lexical scope.
    variable = Box::new(50); // `liveness(variable)` is "LIVE", again. It ows a value.
    // And, thus, it can be used as an operand.
    println!("variable = {}", variable);
} // The lexical scope of `variable` ends here.

/// lifetime vs Liveliness vs lexical scope
fn test2() {
    let mut v1: Box<u8> = Box::new(5);
    let mut v2: Box<u8> = Box::new(10);

    // "Borrows" of `v1` and `v2` take place on the next line of code.
    // The "lifetime" of `b` is intrinsically linked to the "lifetimes" of `v1` and `v2`.
    // <=> `b` ,`v1` and `v2` are << entangled >> (to print the lexicon of quantum mechanics).
    let b: &Box<u8> = entangle_values(&v1, &v2);
    println!("b:{} <-> ({} or {})", b, v1, v2);

    // One of the borrowed values (the one associated with `v1`) moves out "of its owner" (that is,
    // `v1`) on the next line.
    move_value(v1);
    // Now, `liveness(v1)` is "DEAD". We cannot use `v1` *as an operand*, since it does not own a
    // value anymore (the value moved). The line below would generate a compilation error.
    // use_value(v1); // `cargo build` => "value used here after move"

    // The value referenced by `&v1` (or owned by `v1`) moved... Thus, `&v1` is not usable anymore
    // *as an operand*, and thus, neither is `b`. The following line would generate a compilation
    // error.
    // println!("b:{}", b);

    // But `v1` and `v2` are still within the lexical scope.
    v1 = Box::new(50); // `liveness(v1)` is "LIVE" <=> `v1` owns a value.
    v2 = Box::new(110); // `liveness(v2)` is (still) "LIVE" <=> `v2` owns a value.
}

fn main() {
    test1();
    test2();
}

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.