Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why does a mutable reference to a dropped object still count as a mutable reference?

Tags:

rust

lifetime

Here is a simplified example:

struct Connection {}

impl Connection {
    fn transaction(&mut self) -> Transaction {
        Transaction { conn: self }
    }
}

struct Transaction<'conn> {
    conn: &'conn Connection,
}

impl<'conn> Transaction<'conn> {
    fn commit(mut self) {}
}

fn main() {
    let mut db_conn = Connection {};

    let mut trans = db_conn.transaction(); //1
    let mut records_without_sync = 0_usize;
    const MAX_RECORDS_WITHOUT_SYNC: usize = 100;
    loop {
        //do something
        records_without_sync += 1;
        if records_without_sync >= MAX_RECORDS_WITHOUT_SYNC {
            trans.commit();
            records_without_sync = 0;
            trans = db_conn.transaction(); //2
        }
    }
}

The compiler reports two mutable borrows at 1 and 2, but that is not the case. Since trans.commit() takes self by value, trans is dropped, so by point 2 there should be no mutable references.

  1. Why can the compiler not see that at 2 there are no mutable references?
  2. How can I fix the code, leaving the same logic?
like image 404
user1244932 Avatar asked Jun 03 '26 20:06

user1244932


2 Answers

There is a mutable reference.

If you change transaction to this:

fn transaction(&mut self) -> Transaction {
    let _: () = self;
    Transaction{conn: self}
}

You'll see that the compiler errors with:

 = note: expected type `()`
 = note:    found type `&mut Connection`

So self is of type &mut Connection ... a mutable reference. You're then passing this into the Transaction instance that is being returned from this function.

That means your mutable borrow exists for the lifetime of trans (curly braces added by me to show the scope of the borrow):

let mut trans = db_conn.transaction();
{ // <-------------------- Borrow starts here
    let mut records_without_sync = 0_usize;
    const MAX_RECORDS_WITHOUT_SYNC: usize = 100;
    loop {
        //do something
        records_without_sync += 1;
        if records_without_sync >= MAX_RECORDS_WITHOUT_SYNC {
            trans.commit();
            records_without_sync = 0;
            trans = db_conn.transaction();// <--- ####### D'oh! Still mutably borrowed
        }
    }
} // <-------------------- Borrow ends here

If you're looking for this sort of parent-><-child setup, I think you'll have to reach for Rc<RefCell>.

Specifically, an Rc to reference count how many times you pass the connection around and RefCell to track borrowing at runtime instead of compile time. Yes, that does mean you'll panic if you manage to try and mutably borrow it twice at runtime. Without knowing more about your architecture its hard to say whether this is suitable or not.

Here is my solution anyway:

use std::cell::RefCell;
use std::rc::Rc;

struct Connection {}

impl Connection {
    fn do_something_mutable(&mut self) {
        println!("Did something mutable");
    }
}

type Conn = Rc<RefCell<Connection>>;

struct Transaction {
    conn: Conn,
}

impl Transaction {
    fn new(connection: Conn) -> Transaction {
        Transaction { conn: connection }
    }

    fn commit(mut self) {
        self.conn.borrow_mut().do_something_mutable();
    }
}

fn main() {
    let db_conn = Rc::new(RefCell::new(Connection {}));

    let mut trans = Transaction::new(db_conn.clone());
    let mut records_without_sync = 0_usize;
    const MAX_RECORDS_WITHOUT_SYNC: usize = 100;
    loop {
        //do something
        records_without_sync += 1;
        if records_without_sync >= MAX_RECORDS_WITHOUT_SYNC {
            trans.commit();
            records_without_sync = 0;
            trans = Transaction::new(db_conn.clone());
            break; // Used to stop the loop crashing the playground
        }
    }
}
like image 166
Simon Whitehead Avatar answered Jun 06 '26 11:06

Simon Whitehead


Your original code works when non-lexical lifetimes are enabled:

#![feature(nll)]

struct Connection {}

impl Connection {
    fn transaction(&mut self) -> Transaction {
        Transaction { conn: self }
    }
}

struct Transaction<'conn> {
    conn: &'conn Connection,
}

impl<'conn> Transaction<'conn> {
    fn commit(self) {}
}

fn main() {
    let mut db_conn = Connection {};

    let mut trans = db_conn.transaction();
    let mut records_without_sync = 0_usize;
    const MAX_RECORDS_WITHOUT_SYNC: usize = 100;
    loop {
        //do something
        records_without_sync += 1;
        if records_without_sync >= MAX_RECORDS_WITHOUT_SYNC {
            trans.commit();
            records_without_sync = 0;
            trans = db_conn.transaction();
        }
    }
}

Non-lexical lifetimes improve the precision of the borrow checker. The compiler has become smarter and it is now able to prove that more programs are memory safe.

like image 40
Shepmaster Avatar answered Jun 06 '26 10:06

Shepmaster



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!