1

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?

9
  • The scope of second: String does not live long enough as struct My does, so there must exist some time period when My's field second :&String refers to nothing. You probably don't expect such a dangerous case to happen. Commented Oct 18, 2022 at 4:53
  • 1
    @xc wang I would actually expect such a case to happen. if I wanted a single life time I would have written struct My<'a> { first: &'a String, second: &'a String} There is a way to signal the intention that you want the struct to have a single lifetime and if any of the fields are invalidated the struct should be too But the case I am asking is specifically signals the intention that two lifetimes should be independent, yet it behaves just like the single life time case. I do not disagree that that could be a good default but I expect expliciteness to override the default Commented Oct 18, 2022 at 5:12
  • These two struct examples are, in my opinion, talking about distinct things. The first one mentions about a lifetime issue, while the second one mentions about a drop issue. drop second doesn't affect the pointer to first at all, so the second surely would work. It's very confusing that what your point is. Commented Oct 18, 2022 at 16:40
  • @xc wang My point is only about lifetimes. I use the drop function to limit the lifetime of the reference because the valid lifetime of a reference extends so far as its referent is dropped. I drop the referent at some point to show that the lifetime of one of the references ends there but the other references lifetime can exceed that point. Commented Oct 19, 2022 at 0:24
  • No, the case of the second example is not the same as the first one. Note that statements are generally in a top-to-down order. In your first example, the line second: &second is illegal as soon as you are initializing my with a shorter-lived local variable, so you actually failed at the initialization stage. But in the second one, you indeed succeeded at the initialization stage, and drop(my.second) which is equivalent to a partial move operation behaves after the initialization and thus has no effect on the initialization. Commented Oct 19, 2022 at 3:17

2 Answers 2

2

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.

Sign up to request clarification or add additional context in comments.

4 Comments

It is worth noting that partial moving is disabled if the type implements Drop. Otherwise, Own::drop() could receive a partially moved value, and that would be unsound.
@FZs The answer implies that there is no difference between My1<'a,'b>{ f :&'a X, s: &'b Y} and My2<'a>{ f : &'a X, s: &'a Y}. Is that correct?
@adala Yes and no. The struct will be valid for the same time either way. But the latter will shorten the lifetime of the longer-living reference even after they are moved out of the struct, while the former won't. Check My1 vs My2
@rodrigo I didn't even know that - but sounds logical.
0

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.

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.