简体   繁体   中英

Why is my zero-cost alternative to RefCell not the standard way of achieving interior mutability?

I've been thinking about why interior mutability in Rust in most cases requires runtime checks (eg RefCell ). It looks like I've found a safe alternative without a runtime cost. I've called the type SafeCell (mainly because it is a safe wrapper around UnsafeCell ), and it allows you to apply any function to the wrapped value without the risk of having the reference escape:

struct SafeCell<T> {
    inner: UnsafeCell<T>,
}

impl<T> SafeCell<T> {
    pub fn new(value: T) -> Self {
        Self {
            inner: UnsafeCell::new(value),
        }
    }

    pub fn apply<R, F>(&self, fun: F) -> R
    where
        F: FnOnce(&mut T) -> R,
    {
        // Reference below has a lifetime of the current scope, so if
        // user tries to save it somewhere, borrow checker will catch this.
        let reference: &mut T = unsafe { &mut *self.inner.get() };
        fun(reference)
    }
}

This type can be used for interior mutability like this:

pub struct MySet {
    set: HashSet<i32>,
    unique_lookups: SafeCell<HashSet<i32>>,
}

impl MySet {
    pub fn contains(&self, value: i32) -> bool {
        self.unique_lookups.apply(|lookups| lookups.insert(value));
        self.set.contains(value)
    }

    pub fn unique_lookups_count(&self) -> usize {
        self.unique_lookups.apply(|lookups| lookups.len())
    }
}

Or in conjunction with Rc :

fn foo(rc: Rc<SafeCell<String>>) {
    rc.apply(|string| {
        if string.starts_with("hello") {
            string.push_str(", world!")
        }
        println!("{}", string);
    });
}

Playground

  1. Are there any safety/soundness issues with this type?
  2. If not, why is a type like this not a standard way of achieving interior mutability? It looks like it is as usable as RefCell while providing static lifetime checks as opposed to runtime checks.

There is nothing in your API stopping a user from calling apply again in the closure provided to apply . This allows there to be multiple simultaneous mutable references to the same data, which is undefined behavior.

let x = SafeCell::new(0);
x.apply(|y| {
    x.apply(|z| {
        // `y` and `z` are now both mutable references to the same data
        // UB!
        *y = 1;
        *z = 2;
    })
});
x.apply(|y| println!("x: {}", y));

(playground)

Miri correctly calls this out when it sees the second mutable reference being made.

error: Undefined Behavior: not granting access to tag <untagged> because incompatible item is protected: [Unique for <1651> (call 1230)]
  --> src/main.rs:20:42
   |
20 |         let reference: &mut T = unsafe { &mut *self.inner.get() };
   |                                          ^^^^^^^^^^^^^^^^^^^^^^ not granting access to tag <untagged> because incompatible item is protected: [Unique for <1651> (call 1230)]
   |

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM