2

I have the below example where I want a struct which holds a vector of data in one field, and has another field which contains the currently selected field. My understanding is that this is not possible in rust because I could remove the element in tables which selected points to, thereby creating a dangling pointer (can't borrow mut when an immutable borrow exists). The obvious workaround is to instead store a usize index for the element, rather than a &'a String. But this means I need to update the index if I remove an element from tables. Is there any way to avoid this using smart pointers, or just any better solutions in general? I've looked at other questions but they are not quite the same as below, and have extra information which makes them harder to follow for a beginner like myself, whereas below is a very minimal example.

struct Data<'a> {
    selected: &'a String,
    tables: Vec<String>,
}

fn main() {
    let tables = vec!["table1".to_string(), "table2".to_string()];
    let my_stuff = Data {
        selected: &tables[0],
        tables: tables,
    };
}
Max888
  • 3,089
  • 24
  • 55
  • 1
    So long as the Vec holds the value directly, rather than through something like an Rc, then any mechinism that holds a pointer to the value would need to be updated when ever you remove an element from the vec, because the current value could be moved within the vec. So either something like Rc to move the actual value out of the vec, or an index is the way to do it. – user1937198 Apr 24 '22 at 13:46
  • 2
    It all depends on what you want to do with `tables`: can the selected element be removed? can anything be inserted/removed anywhere in `tables`? Or is `tables` immutable? – rodrigo Apr 24 '22 at 13:54
  • 1
    I'm not sure why you are bothered by updating the index whenever an element is removed. Since it's all in a single struct, you can (should?) deny access to `tables`, and only allow modifying it through `Data` methods, which also take care of updating the index accordingly. – jthulhu Apr 24 '22 at 14:09
  • @user1937198 Thanks, I will look into using Rc. Not sure if that means I need to store the actual values somewhere else, and how that stays in sync with the Rc's, but I've never used them before so need to investigate. – Max888 Apr 24 '22 at 14:35
  • @rodrigo tables is mutable, and ideally I can do anything to it I can do to any vector. But the more I think about it the more this seems unreasonable and I probably need to build a specific API like BlackBeans suggests. – Max888 Apr 24 '22 at 14:37
  • @BlackBeans thanks, the more I think about it the seems to be the obvious solution, given there is no way to magically keep the vector and reference in sync, I need to specify the behaviour myself, and I can just reimplement all the Vec methods I need, while taking into account and updating the index field (selected). – Max888 Apr 24 '22 at 14:40
  • 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) If not, please edit your question to explain why. – Chayim Friedman Apr 24 '22 at 15:35
  • 1
    For this kind of problem sometimes I use the [`slotmap`](https://crates.io/crates/slotmap) crate. It is a map-like container, not a vector, but quite convenient because keys are stable even if the container is modified or the selected value removed. – rodrigo Apr 24 '22 at 17:31
  • You could redesign the struct for better encapsulation. For example, we can implement a method `Data::remove` that 1) removes an element from `tables` and 2) updates the internal `selected` state accordingly. – remykarem Aug 09 '22 at 03:16

1 Answers1

3

You quite rightfully assessed that the way you wrote it is not possible, because Rust guarantees memory safety and storing it as a reference would give the possibility to create a dangling pointer.

There are several solutions that I could see here.


Static Strings

This of course only works if you store compile-time static strings.

struct Data {
    selected: &'static str,
    tables: Vec<&'static str>,
}

fn main() {
    let tables = vec!["table1", "table2"];
    let my_stuff = Data {
        selected: &tables[0],
        tables,
    };
}

The reason this works is because static strings are non-mutable and guaranteed to never be deallocated. Also, in case this is confusing, I recommend reading up on the differences between Strings and str slices.

You can even go one further and reduce the lifetime down to 'a. But then, you have to store them as &'a str in the vector, to ensure they cannot be edited. But that then allows you to store Strings in them, as long as the strings can be borrowed for the entire lifetime of the Data object.

struct Data<'a> {
    selected: &'a str,
    tables: Vec<&'a str>,
}

fn main() {
    let str1 = "table1".to_string();
    let str2 = "table2".to_string();
    let tables = vec![str1.as_str(), str2.as_str()];

    let my_stuff = Data {
        selected: &tables[0],
        tables,
    };
}

Reference counting smart pointers

Depending your situation, there are several types that are recommended:

  • Rc<...> - if your data is immutable. Otherwise, you need to create interior mutability with:
  • Rc<Cell<...>> - safest and best solution IF your problem is single-threaded and deals with simple data types
  • Rc<RefCell<...>> - for more complex data types that have to be updated in-place and can't just be moved in and out
  • Arc<Mutex<...>> - as soon as your problem stretches over multiple threads

In your case, the data is in fact simple and your program is single-threaded, so I'd go with:

use std::{cell::Cell, rc::Rc};

struct Data {
    selected: Rc<Cell<String>>,
    tables: Vec<Rc<Cell<String>>>,
}

fn main() {
    let tables = vec![
        Rc::new(Cell::new("table1".to_string())),
        Rc::new(Cell::new("table2".to_string())),
    ];
    let my_stuff = Data {
        selected: tables[0].clone(),
        tables,
    };
}

Of course, if you don't want to modify your strings after creation, you could go with:

use std::rc::Rc;

struct Data {
    selected: Rc<String>,
    tables: Vec<Rc<String>>,
}

fn main() {
    let tables = vec![Rc::new("table1".to_string()), Rc::new("table2".to_string())];
    let my_stuff = Data {
        selected: tables[0].clone(),
        tables,
    };
}

Hiding the data structure and using an index

As you already mentioned, you could use an index instead. Then you would have to hide the vector and provide getters/setters/modifiers to make sure the index is kept in sync when the vector changes.

I'll keep the implementation up to the reader and won't provide an example here :)


I hope this helped already, or at least gave you a couple of new ideas. I'm happy to see new people come to the community, so feel free to ask further questions if you have any :)

Finomnis
  • 18,094
  • 1
  • 20
  • 27
  • The best solution is probably to redesign how you store your data, and if it's impossible to store indices. `Rc` is usually not a good idea, but it may be useful. – Chayim Friedman Apr 24 '22 at 20:43
  • Sure :) I'm just trying to work with the information we are given here – Finomnis Apr 24 '22 at 20:56