The question says "a collection in a struct", but I'm uncertain whether the "in a struct" part is actually relevant. I may not understand the problem well enough to properly ask the question. In any case, I'm trying to do the following:
Create a graph. Each node in the graph has a collection of string identifiers indicating which other nodes it is connected to. Of note: This collection also includes nodes which it is implicitly connected to via other nodes, which it distinguishes by indicating their distance (implicit connections will have distance > 1). Here's the code:
use core::mem::MaybeUninit;
use core::ptr;
use std::collections::HashMap;
use std::sync::Once;
// In my actual code, this map is global for WebAssembly reasons.
// I'm leaving it global to avoid introducing new borrow-checker issues on top of my existing ones.
// Please roll with it. I realize there may be better implementations.
static mut NODE_MAP_UNSAFE: MaybeUninit<HashMap<String, Node>> = MaybeUninit::uninit();
static NODE_MAP_INIT: Once = Once::new();
// For readability
macro_rules! node_map { () => { unsafe { &(*NODE_MAP_UNSAFE.as_ptr()) } }; }
macro_rules! node_map_mut { () => { unsafe { &mut (*NODE_MAP_UNSAFE.as_mut_ptr()) } }; }
// Contains the node's identifier and its connections
struct Node {
name: String,
connections: HashMap<String, Connect>,
}
// Contains the identifier of the node we're connected to, as well as its distance from us
struct Connect {
name: String,
dist: i32,
}
fn main() {
// Again, please roll with it
NODE_MAP_INIT.call_once(|| unsafe { ptr::write(NODE_MAP_UNSAFE.as_mut_ptr(), HashMap::new()); });
// Create nodes 1-3 and add them to the map
let node1 = Node::new("1");
let node2 = Node::new("2");
let node3 = Node::new("3");
node_map_mut!().insert(String::from("1"), node1);
node_map_mut!().insert(String::from("2"), node2);
node_map_mut!().insert(String::from("3"), node3);
// Note: I will be skipping all Option::None checks for brevity's sake.
// The actual code would of course perform the checks properly.
// Connect node 1 to node 2, and vice versa
node_map_mut!().get_mut(&String::from("1")).unwrap().add_connection(String::from("2"));
node_map_mut!().get_mut(&String::from("2")).unwrap().add_connection(String::from("1"));
// Connect node 2 to node 3, and vice versa
node_map_mut!().get_mut(&String::from("2")).unwrap().add_connection(String::from("3"));
node_map_mut!().get_mut(&String::from("3")).unwrap().add_connection(String::from("2"));
// Implicitly connect node 1 to node 3, via node 2
node_map_mut!().get_mut(&String::from("1")).unwrap().update_connections(String::from("2"));
}
impl Node {
fn new(name: &str) -> Node {
return Node {
name: String::from(name),
connections: HashMap::new(),
}
}
// Add a connection
fn add_connection(&mut self, comp_to_add: String) {
// Create the new connection
let new_connection = Connect { name: comp_to_add.clone(), dist: 1 };
// Implicitly connect to distant nodes via our new connection
// No problem here, since the connection doesn't belong to self.connections yet
self.infer(&node_map!().get(&new_connection.name).unwrap().connections);
// Add the connection to self.connections
self.connections.insert(comp_to_add.clone(), new_connection);
}
// Check for new implicit connections and create them, if they exist.
fn update_connections(&mut self, comp_to_update_via: String) {
// Retrieve the existing connection from self.connections (thus borrowing it).
let existing_connection = &self.connections.get(&comp_to_update_via).unwrap().name;
// Implicitly connect to distant nodes via the existing connection
self.infer(&node_map!().get(existing_connection).unwrap().connections); // problem here
}
fn infer(&mut self, map: &HashMap<String, Connect>) {
for element in map {
// Ignore connections to us
if element.0 == &self.name { continue; }
// Add the implicit connection to self.connections (thus borrowing it).
// Actual code would instead update the connection if it already existed.
self.connections.insert(element.0.clone(), Connect { name: element.0.clone(), dist: element.1.dist + 1 });
}
}
}
This gives the following warning:
warning: cannot borrow `*self` as mutable because it is also borrowed as immutable
--> src/main.rs:76:9
|
73 | let existing_connection = self.connections.get(&comp_to_update_via).unwrap();
| ---------------- immutable borrow occurs here
...
76 | self.infer(&node_map!().get(&existing_connection.name).unwrap().connections); // problem here
| ^^^^ ------------------------- immutable borrow later used here
| |
| mutable borrow occurs here
|
= note: `#[warn(mutable_borrow_reservation_conflict)]` on by default
= warning: this borrowing pattern was not meant to be accepted, and may become a hard error in the future
= note: for more information, see issue #59159 <https://github.com/rust-lang/rust/issues/59159>
In my actual code, that's an error, but in this abbreviated version it's a warning for reasons I don't understand (maybe the error is being optimized out?). Either way, the problem is the same: self.connections is being borrowed to retrieve the name, and then self.connections is modified before the borrow ends. I could fix this in either of the following ways:
By cloning the String:
let existing_connection = self.connections.get(&comp_to_update_via).unwrap().name.clone();
self.infer(&node_map!().get(&existing_connection).unwrap().connections);
By doing the entire borrow inline:
self.infer(&node_map!().get(&self.connections.get(&comp_to_update_via).unwrap().name).unwrap().connections);
I dislike option 1 because throwing .clone()s around seems like a bad habit to be in. If the type in question were something more complex than a String, the performance cost could potentially be quite high.
I dislike option 2 because 1: it's ugly and hard to read, and 2: it wouldn't be possible (I don't think) if I were properly checking for Option::None. Also, I don't even understand why it works in the first place.