bzm3r
bzm3r

Reputation: 4596

Getting a value from a child struct, and then modifying the child struct: a borrow checker puzzle?

Consider this toy code:

struct X {
    x: usize,
    times_accessed: usize,
}

impl X {
    fn get(&mut self) -> usize {
        self.times_accessed = self.times_accessed + 1;
        self.x
    }
}

struct Y {
    y: usize,
    max_access: usize,
    option_x: Option<X>,
}

impl Y {
    fn get_x(&mut self) -> Option<usize> {
        match self.option_x {
            Some(ref mut some_x) => {
                let result = some_x.get();

                if some_x.times_accessed == self.max_access {
                    self.option_x = None;
                }

                Some(result)
            }
            None => {
                println!("option_x is not initialized! try calling Y::init_x");
                None
            }
        }
    }

    fn init_x(&mut self, x_val: usize, max_allowed_access: usize) {
        self.max_access = max_allowed_access;
        self.option_x = Some(X {
            x: x_val,
            times_accessed: 0,
        });
    }
}

fn main() {
    println!("hello world!");
}

I haven't bothered using Y in the main function, because the compiler doesn't need me to do that to point out that the borrow checker would not be satisfied with the implementation for Y::get_x:

error[E0506]: cannot assign to `self.option_x` because it is borrowed
  --> src/main.rs:26:21
   |
22 |             Some(ref mut some_x) => {
   |                  -------------- borrow of `self.option_x` occurs here
...
26 |                     self.option_x = None;
   |                     ^^^^^^^^^^^^^^^^^^^^ assignment to borrowed `self.option_x` occurs here

I understand the issue from the borrow checker's perspective, and I can come up with a very simple fix: copy the value from some_x into result (am I not already doing that, after all, result isn't a reference, and usize has the Copy trait), and then "drop the reference" to some_x (would it be using drop(some_x);, so I can modify option_x? A version of that is presented here, but still doesn't work:

fn get_x(&mut self) -> Option<usize> {
    match self.option_x {
        Some(ref mut some_x) => {
            let result = some_x.get();

            if some_x.times_accessed == self.max_access {
                drop(some_x);
                self.option_x = None;
            }

            Some(result)
        }
        None => {
            println!("option_x is not initialized! try calling Y::init_x");
            None
        }
    }
}

What should one do?

Upvotes: 0

Views: 70

Answers (1)

Shepmaster
Shepmaster

Reputation: 430663

As the error message states, you are attempting to replace self.option_x while you have a reference to something inside it:

Some(ref mut some_x) => {
    let result = some_x.get();

    if some_x.times_accessed == self.max_access {
        self.option_x = None; // <---------
    }

    Some(result)
}

Any code after the highlighted point that accesses some_x would get an invalid value. This is how programs crash, expose security holes, etc..

In a potential Rust future, a compiler that understands non-lexical lifetimes might realize that you don't use that value so the code is ok in the current state. Until then, you can take the entire value out of the Option then put it back if your condition isn't met:

match self.option_x.take() {
    Some(mut some_x) => {
        let result = some_x.get();

        if some_x.times_accessed != self.max_access {
            self.option_x = Some(some_x);
        }

        Some(result)
    }

See also:

Upvotes: 1

Related Questions