0

The code goes something like this:

struct Model {
    // This is Vec #1.
    recorded_curves: Vec<Curve>,
    // This is the Vec whose elements hold references to #1's.
    ongoing_comparisons: Vec<Comparison>,
}

struct Comparison {
   // `recording` references an element in `Model.recorded_curves`.
   recording: &Curve,
   // ...
}

And later, inside a function called event(), I have this:

for recording in model.recorded_curves.iter() {
    model.ongoing_comparisons.push(Comparison {
        recording,
        // ... 
    });
}

Rust demands I add lifetimes to Comparison, but to do that, I have to (as far as I know) use the same lifetime for Model. Afterward, the structs looked like this:

struct Comparison<'a> {
    recording: &'a Curve,
    // ...
}

struct Model<'a> {
    recorded_curves: Vec<Curve>,
    ongoing_comparisons: Vec<Comparison<'a>>,
}

fn event<'a>(_app: &App, model: &'a mut Model<'a>, event: Event) { /* ... */ }

Unfortunately, this brings its own problem. I'm using nannou, where I register event() as a callback, and it doesn't let me use my custom lifetime ('a):

error[E0308]: mismatched types
  --> src/main.rs:17:30
   |
17 |     nannou::app(model).event(event).simple_window(view).run();
   |                              ^^^^^ one type is more general than the other
   |
   = note: expected fn pointer `for<'r, 's> fn(&'r nannou::App, &'s mut Model<'_>, nannou::Event)`
              found fn pointer `for<'a, 'r> fn(&'r nannou::App, &'a mut Model<'a>, nannou::Event)`

Is there a way to accomplish this without using lifetime annotations? Otherwise, could I somehow make the program work with the annotations?

verified_tinker
  • 625
  • 5
  • 17
  • 2
    Does this answer your question? [Why can't I store a value and a reference to that value in the same struct?](https://stackoverflow.com/questions/32300132/why-cant-i-store-a-value-and-a-reference-to-that-value-in-the-same-struct) – Chayim Friedman Jul 19 '22 at 16:56
  • @ChayimFriedman What a fantastic answer! However, I'm not so sure it solves my problem (though I'll have to reread it a few times to fully grasp it). The most obvious fixes they suggest are crates that don't work on mutating vectors, which are exactly what I have. Otherwise, they recommend using reference counting. It's great to have a lead, but I wouldn't say it answers my question. – verified_tinker Jul 19 '22 at 17:14
  • If you need to mutate the vectors, and you cannot use reference counting/redesign your code, then you have to use `unsafe`. – Chayim Friedman Jul 19 '22 at 17:16
  • @ChayimFriedman I'll use reference counting if I have to—and if I can—but I think it's fair to say the question, "Is there a way to accomplish this without using lifetime annotations?" means the answer, "Take a look at reference counting," counts as a comment more than an actual answer. – verified_tinker Jul 19 '22 at 17:26
  • If that's the question then the answer is "No". But I do think the duplicate covers that. – Chayim Friedman Jul 19 '22 at 17:28
  • @ChayimFriedman Does it? "There is a special case where the lifetime tracking is overzealous: when you have something placed on the heap. ...Some crates provide ways of representing this case, but they require that the base address never move. This rules out mutating vectors, which may cause a reallocation and a move of the heap-allocated values. ...In other cases, you may wish to move to some type of reference-counting, such as by using `Rc` or `Arc`." I'll reread the answer, but this is what relevant (to this) information I was able to pull out. – verified_tinker Jul 19 '22 at 17:34
  • OK, maybe it does not. I will not close this as a duplicate, but anyway the answer is "No". – Chayim Friedman Jul 19 '22 at 17:36
  • The answer to 'Is there a way to accomplish this without using lifetime annotations?" is: Well, you can't even accomplish this with lifetime annotations. You can only accomplish this with `unsafe` code. This is sadly what happens to most programmers that switch from other languages to Rust: you have to learn new programming paradigms, because many of them aren't considered safe in Rust. For the your specific problem, a two common solutions exist: *Reference Counted Smart Pointers* and *Indices* (meaning: store the position in the array of the item you mean) – Finomnis Jul 19 '22 at 18:04
  • @Finomnis Could you point me to a "Rusty" alternative to this, then? I don't mind learning a new, safer paradigm. If you need more context, you need only ask. – verified_tinker Jul 19 '22 at 18:14
  • As I said, either *Reference Counted Smart Pointers* (`Rc`/`Arc`) or *indices* (as, normal `usize` that store the position of the actual item in the array – Finomnis Jul 19 '22 at 18:21
  • But if you want to stick to the code layout you have on hand, I'd use `Rc`. The overhead is really not big enough to seriously worry about it. – Finomnis Jul 19 '22 at 18:23

1 Answers1

1

There are multiple solutions to this problem, but in your case I would probably use Reference Counting Smart Pointers.

Depending on your usecase, there are several options for the members of your recorded_curves vector:

  • Rc<Curve> - single threaded, immutable
  • Rc<RefCell<Curve>> - single threaded, but mutable. Fails at runtime if you want to borrow it twice simultaneously.
  • Arc<Curve> - multi threaded, immutable
  • Arc<Mutex<Curve>> - multi threaded, mutable, synchronized via mutex

My educated guess is that you probably have a single threaded, but mutable situation, so I'd do:

struct Model {
    // This is Vec #1.
    recorded_curves: Vec<Rc<RefCell<Curve>>>,
    // This is the Vec whose elements hold references to #1's.
    ongoing_comparisons: Vec<Comparison>,
}

struct Comparison {
   // `recording` references an element in `Model.recorded_curves`.
   recording: Rc<RefCell<Curve>>,
   // ...
}

If you had two different structs, one holding the data and one holding the references, and the compiler can prove that the data definitely outlives the references, then it's absolutely no problem to hold references to vector elements.

Here is an example to demonstrate that. It has two structs, DataVec and RefVec, one holding the data and the other one holding mutable references. Then, the RefVec gets passed into a function that sorts it and then stores the sorted positions in the RefVec. You can see that this modifies the original DataVec, so that every element now holds the position it would be in after sorting.

#[derive(Debug)]
struct Data(usize);

#[derive(Debug)]
struct DataVec {
    elements: Vec<Data>,
}

#[derive(Debug)]
struct RefVec<'a> {
    elements: Vec<&'a mut Data>,
}

fn convert_to_order(data: &mut RefVec) {
    data.elements.sort_by_key(|el| el.0);
    for (pos, element) in data.elements.iter_mut().enumerate() {
        element.0 = pos;
    }
}

fn main() {
    let mut datavec = DataVec {
        elements: [5, 8, 3, 7, 9, 4].into_iter().map(|e| Data(e)).collect(),
    };

    println!("Data before: {:?}", datavec);

    let mut refvec = RefVec {
        elements: datavec.elements.iter_mut().collect(),
    };

    println!("Reference Vector: {:?}", refvec);

    convert_to_order(&mut refvec);

    println!("Reference Vector after sorting and labeling: {:?}", refvec);
    println!("Data after: {:?}", datavec);
}
Data before: DataVec { elements: [Data(5), Data(8), Data(3), Data(7), Data(9), Data(4)] }
Reference Vector: RefVec { elements: [Data(5), Data(8), Data(3), Data(7), Data(9), Data(4)] }
Reference Vector after sorting and labeling: RefVec { elements: [Data(0), Data(1), Data(2), Data(3), Data(4), Data(5)] }
Data after: DataVec { elements: [Data(2), Data(4), Data(0), Data(3), Data(5), Data(1)] }

Note that if you'd switch the last two println!()s, you would get an error. You can only read datavec after refvec was dropped, which automatically happens between the two prints. The reason is that once a value is borrowed mutably, you can't access the original value at all until that borrow is returned.

Finomnis
  • 18,094
  • 1
  • 20
  • 27
  • I finally understand how I'd use `Rc` in this case. I'll probably go with that for now and switch to `Arc` once I incorporate multi-threading. This is supposed to be a performance-critical section, so I'm a little worried about the reference counting, but it probably won't become a problem (and I'd probably need to use `Arc` once I added multi-threading anyway). I suppose I'll cross that bridge if or when this bottlenecks the program. Thank you. – verified_tinker Jul 19 '22 at 18:31
  • Well, two annotations for what you just said: 1) don't assume, benchmark ;) and 2) if this is too slow, you probably want to refactor your code in general and avoid the necessity to store things in the `ongoing_comparisons` vector. It's no problem keeping a vector of references, as long as you don't store it in the same struct, but instead in a temporary struct that proveably lives shorter than the struct with the original values. But hard to say without knowing your actual code and project. – Finomnis Jul 19 '22 at 18:34
  • I'll benchmark it for sure. The problem with keeping the two vectors in different structs is that Nannou forces you to use only one struct—`Model` in my case—for your data. At least, I don't know how to change that behavior. Neat separation of concerns, but evidently also limiting. I'm also considering converting `recorded_curves` into an array, but I don't know if that's possible, since its size is determined at runtime and depends on the number of files in a directory (which it reads and stores in memory). – verified_tinker Jul 19 '22 at 18:42
  • As I don't think Nannou is geared towards maximum performance, I don't think the use of `Rc` will be much of a problem. – Finomnis Jul 19 '22 at 18:55
  • That's true. I'm only prototyping for now. Most likely, once I'm done and integrate the code into a game engine, I'll be free to refactor it. – verified_tinker Jul 19 '22 at 18:57
  • 1
    Don't change `recorded_curves` to an array. Arrays are stored on the stack and serve a very specific purpose. For everthing even remotely more general-purpose, use vecs instead, as they live on the heap. – Finomnis Jul 19 '22 at 19:00
  • 1
    @verified_tinker Added an example to demonstrate how it would work if you split your struct in two. – Finomnis Jul 19 '22 at 19:16