Jgdo
Jgdo

Reputation: 43

Borrow a constant struct field together with mut self&

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.

Upvotes: 0

Views: 885

Answers (4)

zxch3n
zxch3n

Reputation: 387

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");
}

Upvotes: 1

Jmb
Jmb

Reputation: 23244

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.

Upvotes: 1

YthanZhang
YthanZhang

Reputation: 410

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);
}

Upvotes: 0

Miiao
Miiao

Reputation: 998

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");
}

Upvotes: 0

Related Questions