Are allocation and mutability not orthogonal?

I wonder if I misunderstand this: the only way to have a struct with a mutable array in it, without making the entire struct mutable, is to allocate the array separately, like this:

struct MyStruct {
  x: i32,
  y: RefCell<[i32; MY_ARRAY_LENGTH]>,
}

and then I can use y.borrow_mut() to change the values of the array elements.

Is this correct? Because I think this workaround precludes using #[derive(Default)] on MyStruct, which I would be able to use (I think) without the RefCell, in which case the array would be allocated within the structure.

Is it really impractical or ineffective to allow the declaration of partially mutable objects without the Cell/RefCell constructs?

(My example is slightly different and I didn't test this actual code, so apologies for any mistake.)

Thanks!

1 Like

There’s no such thing as per-field mutability in Rust. By using interior mutability (eg RefCell) you allow shared/aliased mutation (ie don’t need to hold a &mut). So the consideration here is whether you want to allow this type of mutation or not; it doesn’t really have anything to do with per-field vs whole struct mutability.

RefCell does not introduce a "separate allocation" for its contents like Box, Rc, etc.; it stores the contents in place, along with a flag for whether they're currently being borrowed.

RefCell does impl Default when its contents do, so using it shouldn't prevent you from deriving Default.

2 Likes

Thank you for your help in understanding this. As it turn out, part of my confusion came from the fact that

#[derive(Clone,Copy,Default)]
struct S1 {
    x: [u32; 100],
}

does not compile, but changing the array size to 10 does. Some searching showed that Default is implemented for arrays up to 32. Ouch! :confused: Should probably give up and implement it in the compiler---at least as a helpful error message.

To expand on this, one way to allocate and initialize an instance of S2 below:

struct S1 {
a: [u8; 100],
}

impl S1 {
    fn new() -> S1 {
        let x: u8 = Default::default();
        return S1 { a: [x; 100] };
    }
}

struct S2 {
    s1: RefCell<S1>,
}

I can use this code:

let s1 = S1::new();
let s2 = S2 { s1: RefCell::new(s1) };

but now, how can I tell if the construction of S2 implies copying s1? And if it does, is it possible to avoid the copying and how?

You're at the mercy of the optimizer here. In particular, debug mode will almost certainly copy. The best you can do is encourage the optimizer to get things right, and use an rvalue (i.e. a temp) instead of binding to a named variable.

On stable Rust, there's not even a way to ensure that Box::new([0u8; 1024 * 1024]) won't first try to create the array on the stack (and possibly overflow it) and then copy it to the heap.

  • If you're new to Rust and you're not entirely sure you really need an array (e.g. for reading data from C), avoid Rust arrays. They're the least usable type in Rust, and 99% of the time you'll want to use a Vec instead.

  • In Rust mutability is not the property of data, but the way you access it — whether it's a mutable or immutable reference. Additionally, you can always mutate data you own exclusively (let mut foo = foo makes immutable foo mutable!), so there's no problem in mutating the struct to fill in the data, and then sharing it using immutable reference.

Thanks. I am not seeing how Vec would help in this case, though. I only need one instance of a statically-allocated array of fixed, known size.

Re mutability: thanks, I had not absorbed this, I'll play with it. But if that's the case, what is the point of Cell and RefCell then???

https://doc.rust-lang.org/book/second-edition/ch15-05-interior-mutability.html

Vec helps in the sense that it's just easier to use and initialize. It can be created with a predefined capacity, too, although it uses the heap. There's also a 3rd party ArrayVec type that stores the vec inline/on stack if you need that.

Without RefCell each piece of data can have only one (exclusive) mutable reference at a time, which is verified at compile time.

RefCell/Cell is to "cheat" the borrow checker to allow multiple mutable references to exist for the same piece of data (you can make a shared "immutable" reference mutable, at cost of a runtime correctness check). It's used for complex cases that borrow checker can't understand. It's also useful to make methods that are logically immutable to the caller, but still need to be mutable in the implementation (e.g. memorization, lazy initialization, caches)