adal
adal

Reputation: 51

Rust multiple lifetimes in structs

I have problems with understanding the behavior and availability of structs with multiple lifetime parameters. Consider the following:

struct  My<'a,'b> {
    first: &'a String,
    second: &'b String
}

fn main() {
    let my;
    let first = "first".to_string();
    {
        let second = "second".to_string();
        my = My{
            first: &first,
            second: &second
        }
    }
    println!("{}", my.first)
}

The error message says that

   |
13 |             second: &second
   |                     ^^^^^^^ borrowed value does not live long enough
14 |         }
15 |     }
   |     - `second` dropped here while still borrowed
16 |     println!("{}", my.first)
   |                    -------- borrow later used here

First, I do not access the .second element of the struct. So, I do not see the problem.

Second, the struct has two life time parameters. I assume that compiler tracks the fields of struct seperately. For example the following compiles fine:

struct  Own {
    first:  String,
    second: String
}

fn main() {
    let my;
    let first = "first".to_string();
    {
        let second = "second".to_string();
        my = Own{
            first: first,
            second: second
        }
    }
    std::mem::drop(my.second);
    println!("{}", my.first)
}

Which means that even though, .second of the struct is dropped that does not invalidate the whole struct. I can still access the non-dropped elements.

Why doesn't the same the same work for structs with references?

The struct has two independent lifetime parameters. Just like a struct with two type parameters are independent of each other, I would expect that these two lifetimes are independent as well. But the error message suggest that in the case of lifetimes these are not independent. The resultant struct does not have two lifetime parameters but only one that is the smaller of the two.

If the validity of struct containing two references limited to the lifetime of reference with the smallest lifetime, then my question is what is the difference between

struct My1<'a,'b>{
 f: &'a X,
 s: &'b Y,
}

and

struct My2<'a>{
 f: &'a X,
 s: &'a Y
}

I would expect that structs with multiple lifetime parameters to behave similar to functions with multiple lifetime parameters. Consider these two functions

fn fun_single<'a>(x:&'a str, y: &'a str) -> &'a str {
    if x.len() <= y.len() {&x[0..1]} else {&y[0..1]}
}

fn fun_double<'a,'b>(x: &'a str, y:&'b str) -> &'a str {
    &x[0..1]
}

fn main() {
    let first = "first".to_string();
    let second = "second".to_string();
    
    let ref_first = &first;
    let ref_second = &second;
    
    let result_ref = fun_single(ref_first, ref_second);
    
    std::mem::drop(second);
    
    println!("{result_ref}")
}

In this version we get the result from a function with single life time parameter. Compiler thinks that two function parameters are related so it picks the smallest lifetime for the reference we return from the function. So it does not compile this version.

But if we just replace the line

let result_ref = fun_single(ref_first, ref_second);

with

let result_ref = fun_double(ref_first, ref_second);

the compiler sees that two lifetimes are independent so even when you drop second result_ref is still valid, the lifetime of the return reference is not the smallest but independent from second parameter and it compiles.

I would expect that structs with multiple lifetimes and functions with multiple lifetimes to behave similarly. But they don't.

What am I missing here?

Upvotes: 1

Views: 573

Answers (2)

FZs
FZs

Reputation: 18619

I assume that compiler tracks the fields of struct seperately.

I think that's the core of your confusion. The compiler does track each lifetime separately, but only statically at compile time, not during runtime. It follows from this that Rust generally can not allow structs to be partially valid.

So, while you do specify two lifetime parameters, the compiler figures that the struct can only be valid as long as both of them are alive: that is, until the shorter-lived one lives.

But then how does the second example work? It relies on an exceptional feature of the compiler, called Partial Moving. That means that whenever you move out of a struct, it allows you to move disjoint parts separately.

It is essentially a syntax sugar for the following:

struct  Own {
    first:  String,
    second: String
}

fn main() {
    let my;
    let first = "first".to_string();
    {
        let second = "second".to_string();
        my = Own{
            first: first,
            second: second
        }
    }

    let Own{
        first: my_first,
        second: my_second,
    } = my;

    std::mem::drop(my_second);
    println!("{}", my_first);
}

Note that this too is a static feature, so the following will not compile (even though it would work when run):

struct  Own {
    first:  String,
    second: String
}

fn main() {
    let my;
    let first = "first".to_string();
    {
        let second = "second".to_string();
        my = Own{
            first: first,
            second: second
        }
    }

    if false {
        std::mem::drop(my.first);
    }
    println!("{}", my.first)
}

The struct may not be moved as a whole once it has been partially moved, so not even this allows you to have partially valid structs.

Upvotes: 2

Filipe Rodrigues
Filipe Rodrigues

Reputation: 2197

A local variable may be partially initialized, such as in your second example. Rust can track this for local variables and give you an error if you attempt to access the uninitialized parts.

However in your first example the variable isn't actually partially initialized, it's fully initialized (you give it both the first and second field). Then, when second goes out of scope, my is still fully initialized, but it's second field is now invalid (but initialized). Thus it doesn't even let the variable exist past when second is dropped to avoid an invalid reference.

Rust could track this since you have 2 lifetimes and name the second lifetime a special 'never that would signal the reference is always invalid, but it currently doesn't.

Upvotes: 0

Related Questions