0

I'm new to Rust and trying to understand how to properly handle the following borrow checker "problem":

I have a struct containing some fields, and I would like some of the fields to be constant over the lifetime of the object (const in C++). This way, I could borrow &mut self while also holding an immutable reference to the const field.

Here is a minimal example:

use std::collections::HashMap;

struct Process {
    const_data: HashMap<String, i32>, // this is supposed to never be changed after construction!
    mut_data: i32, // this is supposed to be mutated during object lifetime
}


impl Process {
    fn do_more(&mut self, num: &i32) {
        self.mut_data += num;
    }

    fn do_somethig(&mut self, name: &str) {
        let num = self.const_data.get(name).unwrap(); // reference to part of self.const_data
        self.do_more(num); // borrowing &mut self
    }
}

fn main() {
    let mut p = Process { const_data: HashMap::from([
        ("foo".to_string(), 1),
        ("bar".to_string(), 2),
        ("buzz".to_string(), 3),
    ]), mut_data: 0 };

    p.do_somethig("foo");
}

It fails to compile with the error message

error[E0502]: cannot borrow `*self` as mutable because it is also borrowed as immutable
  --> src/main.rs:16:9
   |
15 |         let num = self.const_data.get(name).unwrap();
   |                   ------------------------- immutable borrow occurs here
16 |         self.do_more(num);
   |         ^^^^^-------^^^^^
   |         |    |
   |         |    immutable borrow later used by call
   |         mutable borrow occurs here

Is there a way to mark Process::const_data as immutable, such that the borrow checker knows that its not contradicting the "one mutable reference" principle?

PS: In the example above we could make Process::do_more() take num by value and not by reference, however, in my actual project const_data would contain a complex struct which should not be copied or moved.

4 Answers 4

3

You can use Rc to wrap your const_data. Rc::clone is a cheap operation.

use std::{collections::HashMap, rc::Rc};

struct Process {
    const_data: Rc<HashMap<String, i32>>, // this is supposed to never be changed after construction!
    mut_data: i32,                        // this is supposed to be mutated during object lifetime
}

impl Process {
    fn do_more(&mut self, num: &i32) {
        self.mut_data += num;
    }

    fn do_somethig(&mut self, name: &str) {
        let map = Rc::clone(&self.const_data);
        let num = map.get(name).unwrap(); // reference to part of self.const_data
        self.do_more(num); // borrowing &mut self
    }
}

fn main() {
    let mut p = Process {
        const_data: Rc::new(HashMap::from([
            ("foo".to_string(), 1),
            ("bar".to_string(), 2),
            ("buzz".to_string(), 3),
        ])),
        mut_data: 0,
    };

    p.do_somethig("foo");
}

Sign up to request clarification or add additional context in comments.

1 Comment

Thanks, that perfectly solves the issue!
1

You can use a RefCell to store the mutable portion of the struct, and then take a non-mutable reference to self:

use std::cell::RefCell;
use std::collections::HashMap;

struct Process {
    const_data: HashMap<String, i32>, // this is supposed to never be changed after construction!
    mut_data: RefCell<i32>, // this is supposed to be mutated during object lifetime
}


impl Process {
    fn do_more(&self, num: &i32) {
        *self.mut_data.borrow_mut() += num;
    }

    fn do_somethig(&self, name: &str) {
        let num = self.const_data.get(name).unwrap(); // reference to part of self.const_data
        self.do_more(num); // borrowing &mut self
    }
}

fn main() {
    let p = Process { const_data: HashMap::from([
        ("foo".to_string(), 1),
        ("bar".to_string(), 2),
        ("buzz".to_string(), 3),
    ]), mut_data: RefCell::new (0) };

    p.do_somethig("foo");
}

Playground

PS: For an i32 you would probably use Cell instead of RefCell, but I'm assuming that mut_data is really a more complex type where RefCell is justified.

1 Comment

Thanks, this would indeed work. However, in my case it would be very inconvenient to make do_more() operate on self& instead of &mut set, since there are actually a bunch of those functions.
0

You need to copy your value. This should work:

use std::collections::HashMap;

struct Process {
    const_data: HashMap<String, i32>, 
    mut_data: i32, 
}


impl Process {
    fn do_more(&mut self, num: &i32) {
        self.mut_data += num;
    }

    fn do_somethig(&mut self, name: &str) {
        let ref num = self.const_data[name].clone(); // Reference to value similar to *self.const_data.index(name)
        self.do_more(num);
    }
}

fn main() {
    let mut p = Process { const_data: HashMap::from([
        ("foo".to_string(), 1),
        ("bar".to_string(), 2),
        ("buzz".to_string(), 3),
    ]), mut_data: 0 };
    p.do_somethig("foo");
}

1 Comment

Unfortunaly, this isn't an option, since the data in const_data is in reality a complex struct which definitely should not be copied just to make the program compile.
0

The reason your code doesn't work is because the borrow checker doesn't check if the &mut Process borrow actually modifies what part of the data. It just sees multiple borrows with one of them being mutable and that's not allowed.

The solution is to attach the do_more method directly to mut_data, like so:

use std::collections::HashMap;

#[derive(Debug)]
struct MutData {
    data: i32
}
impl MutData {
    fn do_more(&mut self, num: &i32) {
        self.data += num;
    }
}

struct Process {
    const_data: HashMap<String, i32>, // this is supposed to never be changed after construction!
    mut_data: MutData, // this is supposed to be mutated during object lifetime
}


impl Process {
    fn do_more(&mut self, num: &i32) {
        self.mut_data.do_more(num);
    }

    fn do_somethig(&mut self, name: &str) {
        let num = self.const_data.get(name).unwrap(); // reference to part of self.const_data
        self.mut_data.do_more(num); // borrowing &mut self.mut_data
    }
}

fn main() {
    let const_data = HashMap::from([
        ("foo".to_string(), 1),
        ("bar".to_string(), 2),
        ("buzz".to_string(), 3),
    ]);
    let mut p = Process { const_data, mut_data: MutData { data: 0 } };
    
    println!("{:?}", p.mut_data);

    p.do_somethig("bar");
    
    println!("{:?}", p.mut_data);
}

3 Comments

Thanks, but this wouldn't work for my actual project, since do_more() would eventually require the const data of the whole Process.
Or maybe self.mut_data.do_more(&self.const_data)?
Technically yes, but in practice this would require to propagate the const data through a whole series of function calls. The answer from zxch3n alrady points out a very good solution to the problem.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.