Lifetimes when returning a value and references to that value

I am trying to wrap my head around some of the trickier parts of rust. Here is a simple example:

Playground

For this code:

use std::rc::Rc;

#[derive(Debug)]
struct DaMain {}

#[derive(Debug)]
struct DaRefStruct<'a> {
    da_main: &'a DaMain,
}

#[derive(Debug)]
struct DaOuter<'a> {
    da_ref_struct: DaRefStruct<'a>,
    da_main: Rc<DaMain>,
}

fn make_da_ref_structy<'a>(da_main: &'a DaMain) -> DaRefStruct<'a> {
    DaRefStruct { da_main }
}

fn da_whole_shebang() -> DaOuter<'static> {
    let da_main = Rc::new(DaMain {});

    // let da_main_clone = Rc::clone(&da_main);
    let da_ref_struct = make_da_ref_structy(da_main.clone().as_ref());
    DaOuter { da_ref_struct, da_main }
}

fn main() {
    let da_outer = da_whole_shebang();
    println!("Hello, world! {:?}", da_outer);
}

I am getting this error:

error[E0515]: cannot return value referencing temporary value
  --> examples/borrows_and_also_lifetimes_3.rs:25:5
   |
24 |     let da_ref_struct = make_da_ref_structy(da_main.clone().as_ref());
   |                                             --------------- temporary value created here
25 |     DaOuter { da_ref_struct, da_main }
   |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ returns a value referencing data owned by the current function

The DaMain, and DaRefStruct are external, and I cannot change them. They are a DuckDB Connection object and this appender:

pub struct Appender<'conn> {
    conn: &'conn Connection,
    app: ffi::duckdb_appender,
}

I am trying to find a way of handing back an object from a new method in my code just like in the example. Is what I'm trying to do even possible? Create and move a struct instance out of a function to a caller, while also moving back a few structs that contain references to the original one? It seems like I'm missing something simple. I may be wrong but I would think that this code is "safe" in that I think since da_main and all references to it are moved back out of the function the caller will take ownership of everything. That could be totally incorrect though.

Before introducing the Rc wrapper around the DaMain I was also getting

error[E0515]: cannot return value referencing local variable `da_main`
  --> examples/borrows_and_also_lifetimes_3.rs:25:5
   |
24 |     let da_ref_struct = make_da_ref_structy(&da_main);
   |                                             -------- `da_main` is borrowed here
25 |     DaOuter { da_ref_struct, da_main }
   |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ returns a value referencing data owned by the current function

But the Rc was just me trying random things out to get rid of it.

No.[1] At least not without leaking, which is almost never desired. The Rc is the only thing granting you access to the DaMain, and it drops by the end of da_whole_shebang. The cloning of the Rc doesn't matter; both will drop by the end of the function.

Also generally no. Moving something invalidates references to things within it. Returning an owner and a reference to an owner would be equivalent to returning a self-referential struct. It's not possible without unsafe, and encapsulating self-referential structs with unsafe is notoriously hard to get correct. (There are a few dedicated crates that try to do so, which may or may not be completely sound currently.)

When I say "reference" and so on here, I mean those &/&mut things, not something more general necessarily. For example there may be a mappable Rc crate that would allow you to returned the Rc and a mapped Rc to some field.

Let's say you gave me a Box<T> and a &T to the contents simultaneously. The ownesrhip of the box means I can get an exclusive reference (&mut) to the T, I can move the T out of the box, I can deallocate the Box<T> (dropping the T)... all things incompatible with T being borrowed by the &T.


  1. Judging by the playground ↩ī¸Ž

1 Like

There may be some fundamental misunderstandings. These are somewhat shots in the dark, but...

  • Rust lifetimes -- those '_ things -- primary track the duration of borrows or describe the validity of types. They are not the same as the liveness scope of values.

  • Ascribing lifetimes can change what compiles or not, but it doesn't effect the semantics of your program, like when values drop. You can't make a value stick around longer just by changing lifetime annotations.

2 Likes

And the reference would then point to invalid memory (the previous location of the now-returned value in the now-destroyed stack frame).

References aren't magic. They are just pointers. There's nothing that would suddenly make a reference re-point to the new location of the referent if it (the referent) were moved.

Have you used garbage collected languages earlier? With a GC, a reference to an object causes that object to stay alive. So it might be tempting to think the same happens in Rust, but of course it doesn't as others have explained.

Not only that, but in a GC language, everything is necessarily behind a reference. "Keeping alive by reference" doesn't even make sense if some values are not behind indirection.

1 Like

First of all thank you to everyone. I appreciate everyone taking time to explain all of these in such detail. This will not be a complete response as I'm still grokking all the very thorough replies and going back to reference the book. You were all spot on, I am primarily a Java/JavaScript developer of many years. I have to do some brain breaking for sure.

@quinedot That link, and your book are getting bookmarked :+1:. The explanation at the end of your first message mostly clarified it for me.

Same to you paramagnetic and jumpnbrownweasel

Thanks for the warm welcome :heart::crab:

2 Likes

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.