Rust noob asks: lifetime of a reference in a struct

Hi all! First post here, started with my first Rust experiment a few days ago and now I’m struggling with lifetimes.

I want to have a HashMap with a struct (chrono::NaiveDate) as key and another struct (DayDescriptor) as value. The value struct DayDescriptor shall keep a reference to the key struct.

This is what I tried:

struct DayDescriptor<'a> {
   naive: &'a NaiveDate,
}

pub struct Calendar <'a> {
   today: NaiveDate,
   focusday: NaiveDate,
   days: HashMap<NaiveDate, DayDescriptor<'a>>
}


impl Calendar {
   pub fn new(today: &NaiveDate, focusday:&NaiveDate) -> Calendar {
       let mut calendar = Calendar {
           days: HashMap::new()
       };
       calendar
   }

Well, the compiler of course claims a missing lifetime specification when I try to create an instance of Calendar. What I would like to tell the compiler is that the lifetime 'a must be the lifetime of the maps key. Does that make sense? How can I do that? Or how can I at least use the lifetime of the HashMap or the Calendar struct itself? I don’t want to bother the “user” of this struct (me - tomorrow) with some lifetime stuff if the lifetime can be specified within the struct.

Thanks!

lifetime <'a> is "inside" the new function. Once you leave that closure (i.e., after the last instruction), the lifetime changes to some lifetime that is greater than <'a>. Once the closure ends, the lifetime a expires, in other words. This means that you cannot return calendar as it stands now. What are you trying to do exactly? You pass today and focusday, but are unused variables.

Also, according to: chrono::naive::NaiveDate - Rust

NaiveDate implements Clone. This means you can store NaiveDate by value instead of by reference. This would mean your code simplifies to:

struct DayDescriptor {
   naive: NaiveDate,
}

pub struct Calendar {
   today: NaiveDate,
   focusday: NaiveDate,
   days: HashMap<NaiveDate, DayDescriptor>
}
1 Like

I agree with @nologik. When you have the choice to store something as-is or as a reference, prefer the first option. Storing references inside any longer-lived struct is usually the wrong way to go about it. Clone/Copy/Rc are great tools at our disposal to avoid non-'static structs. Use them until you're more familiar with Rust or at the least, treat them as optimizations, i.e. get your program to run first, optimize later when needed.

1 Like

You cannot use ordinary references to point to other fields within the same struct. What if someone moves the Calendar? Then the pointer would no longer point at the new location for the today field.

Notice that NaiveDate is Copy: chrono::naive::NaiveDate - Rust

That means it's trivial to copy around, so there's generally no value in storing a reference to it, since you could just have a copy instead.

If you're used to other languages where everything is references you might just be used to having references to things, but in Rust make sure you really need them. Often a simple .clone() will make your life way easier without material downside -- and it might even make things smaller and faster.

To re-use one of my favourite recent lines from this forum,

3 Likes

Thanks for the replies!

I was aware that I can clone NaiveDate, it just feels cleaner for me not to duplicate data with one identity. Redundancy in a data model just feels error prone for me.

However, it’s a learning project, so I can live with that one. But I fear there will be more compromises I’ll have to accept then.

In a next step, I wanted to put the HashMap values in linked lists, so that I can both, easily (O(log n)) pick a single DayDescriptor via the HashMap and easily (O(1)) pick a DayDescriptor for a subsequent day, if it already exists in the map. Again, that would need some references.

That’s really a pity. For my understanding, this makes a huge amount of efficient data models impossible. And in my experience, well performing algorithms are more important than the speed of the language in many cases. Anyway, I hope that I will soon think that Rust is easy. :wink:

Those efficient data models you think of are rarely implemented using ordinary references in Rust. References are not for data structures, but for temporary views into other values.

2 Likes

What else is used in Rust then? I mean, even for a simple linked list, I need to “link” to the next element somehow, for doubly linked lists, I need already two “links”. What do I use if I don’t use references? Or am I too biased from my former languages?

Depending on your use-case, you might get away with std::collections::BTreeMap, or something like indexmap. Ultimately, these data structures make use of raw pointers, but expose a memory safe API.

I think many people fall in the trap of thinking of Rust borrows as C++ references (I know I did!). If you do, you'll be sorry. :wink: Rust references are statically asserted to be safe at compile time. As you might expect, proving, at compile time, that the references in an arbitrary combination-linked-list-hash-map are valid is intractable.

Pushing borrow-checking to runtime using the various Cell (edit: I misspoke a little here -- Cell is a little different, and allows for zero-runtime cost operations. For runtime so-called internal mutability, look at types like RefCell, Mutex and RwLock.) types is an option, but comes with runtime cost. The only other option is is build a safe abstraction using operations that can't be proved-to-be-safe.

What this really means is crafting a safe data structure in Rust may be quite a bit more challenging than creating a similar unsafe data structure in C++.

(NB: I have written very little unsafe code, myself, but made use of the many tools others have created.)

1 Like

You might like Learn Rust With Entirely Too Many Linked Lists. :slight_smile:

3 Likes

Typically when one passes floats, ints, even structs to functions in a programming language the data is copied, pass by value. As it when you put them into array elements and so on. I presume you have never worried about that data duplication.

You would not expect to have only one copy of a value and pass pointers/references to it functions, pass by reference. You would not expect to have arrays full of pointers to/reference to every integer you stored in an element.

So I look at cloning NaiveData in Rust the same way. If your data is not some huge object siting in heap that would be inefficient to clone, then why not? I have yet to find this error prone, no more than pass by value as described above.

I very much doubt one needs to be creating linked lists and such in application code in Rust. There are crates available that do all that kind of thing. Of course if you have some cunning 'well performing' algorithm for some funky data structure like that, create a crate for it, use all the "unsafe" and other ticks Rust offers to make it. Perhaps not a job for a noob.

You're confusing Rust's references with pointers.

  • & reference = borrowing, shared pointer
  • &mut reference = borrowing, exclusive pointer
  • Box = owning, exclusive pointer
  • Arc = owning, shared pointer
  • *mut/*const = pointer where ownership and exclusivity is unknown and not checked.

They are all identical to *T pointer in C, and compile to a number usize large (or for trait objects or slices, two usizes).

There are multiple ways to "reference" objects in Rust, and Rust's & (temporary borrow) is only one of them. You can't use it for everything. Without a GC, it matters how you reference things.

For example, linked lists refer to other items using a pointer, but they are not temporarily borrowing these items. They own these items, either exclusively (singly-linked) or have shared ownership (doubly-linked).

2 Likes

Thanks @uberjay for the link and for pointing out that looking for more non-std collection types might be a good idea. It feels that Rust forces me to separate data structure logic more from the application structures than I’m used to. Maybe that’s a good thing.
(I’m curious about have much stuff I will have read when my first 100 lines of Rust code are working. It’s really not that easy...)

@ZiCog: Got your point, but I really think that’s a different story. First, when I’m in a OO world (did a lot of Python in the last years), I mostly pass object references around, basic data types mostly appear only as object attributes. And it’s a different story if I just call a function and I have a temporary copy of a primitive type in an enclosed and well-defined scope, or if my central data model has redundant copies of complex types which potentially live a long time and have to be actively “synchronized” by program logic.

Oh! Thanks for that overview including Arc and *x, @kornel!

A python object reference is roughly equivalent to an Rc<RefCell<T>> in Rust: it’s (mostly) restricted to a single thread, uses reference counts to determine when to free the object, and allows anyone with the reference to modify the object. Rust separates these abilities out so that you don’t pay the runtime cost for the ones you don’t need.

The important point here, to me, is that you had it behind a &, which means that it cannot change anyway. Normalizing the data model is potentially helpful for updates, but it cannot be updated behind a &.

And note that even in something like C# -- with a GC and no lifetime complications -- a DateTime is still a struct that gets copied, not an object that typically get shared. It's possible to do so, by wrapping it into a class, but that doesn't seem to a thing that's normally done. Nor are dates split out into a separate table in SQL, in my experience.

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.