const TEST: RefCell<Option<i32>> = RefCell::new(None);
fn main() {
let _ = TEST.borrow_mut().insert(42);
match TEST.borrow().as_ref() {
Some(x) => println!("Value is: {:?}", x),
None => unreachable!(),
}
}
Why is it even possible/allowed to create a constRefCell?
Attempting to modify the "inner" value of the constRefCell neither gives a compiler error, nor does it give an error (panic) at runtime. Still, it obviously does not work! It just fails silently
I mean, I understand that modifying a const value is not possible. But, on the other hand, how does a constRefCell ever make sense then? And why is this not prevented at compile-time?
I seems weird that insert() can be called without error, but simply does not have the intended effect.
Isn't this sort of "unexpected" behavior supposed to be impossible in Rust?
const items looks remarkably similar to static items, which introduces some confusion as to which one should be used at which times. To put it simply, constants are inlined wherever they’re used, making using them identical to simply replacing the name of the const with its value.
and it becomes obvious that it panics at runtime, as expected.
But it does not panic.
Well, it only panics because I have put an additional unreachable!() in the branch that should never be reached (but unexpectedly is). It does not panic by itself, because we tried to modify a const value.
To put it simply, constants are inlined wherever they’re used, making using them identical to simply replacing the name of the const with its value.
Ah, I see! This explains the observed behavior.
Still, how can a constRefCell ever make sense then? And why was this not prevented?
I mean, as long as a const value is "truly" constant, it does not make any difference whether we have just a single global instance, of whether it actually gets inlined at every place where it is used.
But, as soon as a const value can be modified (like RefCell), the "inlining" creates lots of confusion!
After all, the observed runtime behavior is very unexpected and misleading...
As can be seen in the expansion of the code, everytime you reference the const value a "new" RefCell is created. Because the second RefCell has nothing to do with the first (apart from them having the same const initializer), there is no way mutation of one could influence the other. Global mutation is not possible (nor should it be) with const, for that static mut exists which is unsafe for several reasons.
I agree that a warning or another hint would be a good option, as this could seem confusing to a newcomer. I think the relevant issue is #40543, although there doesn't seem to be a lot of recent development.
warning: a `const` item with interior mutability should not be borrowed
--> src/main.rs:6:13
|
6 | let _ = TEST.borrow_mut().insert(42);
| ^^^^
|
= help: assign this const to a local or static variable, and use the variable here
= help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#borrow_interior_mutable_const
= note: `#[warn(clippy::borrow_interior_mutable_const)]` on by default
warning: a `const` item with interior mutability should not be borrowed
--> src/main.rs:7:11
|
7 | match TEST.borrow().as_ref() {
| ^^^^
|
= help: assign this const to a local or static variable, and use the variable here
= help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#borrow_interior_mutable_const
Don't use static mut for mutable globals, use a regular static with a proper synchronization primitive like an Rwlock or a Mutex. Much safer and less error-prone.
But what the code effectively expands to is clearly very different form the intent expressed in the original code. So wouldn't it be better/reasonable to simply disallow this and make it a compile-time error?
I mean, the whole point of having a Cell or RefCell is that we can modify its "inner" content. But, if every access to the constRefCell creates a totally new RefCellin-place, then the whole purpose of using a RefCell is contradicted – as any modification we might do will be inevitably be "lost"
This is not possible, because it would break backwards compatibility (i.e. code that was written for a previous version of Rust that relied on this would fail to compile).
A warning might be good, but as you can see in the discussion of the linked issue #40543 one problem is that there is no way to statically determine whether a given type uses interior mutability - another crate could export a RefCell-ish or Mutex-like type and the lint would not work. The clippy lint @steffahn mentioned might also be sufficient (for the standard library and selected crates) for now.
It's not public, but there's a trait for no-interior-mutability (lamentably called Freeze at the moment). More generally, the compiler has to understand where it exists one way or another. That's how the clippy lints are implemented, by asking the compiler if there's any interior mutability types involved.
There's multiple types of analysis w. r. t. interior mutability. Next to the type-based analysis there's also a value based one. E. g. the first of these two consts compiles fine
error[E0492]: constants cannot refer to interior mutable data
--> src/lib.rs:3:30
|
3 | const D: &Option<Cell<()>> = &Some(Cell::new(()));
| ^^^^^^^^^^^^^^^^^^^^ this borrow of an interior mutable value may end up in the final value
I haven't tested which one the Clippy lint uses. I'm also not sure off the top of my head in what situstions the Freeze trait is used. (Feel free to point out an example of an error that's clearly based on such a type based analysis, if you know one.) Well, some relevance at least: Of course, the type based analysis is the one necessary for code generation, to inform LLVM about the possibility of aliased mutation.
So the question becomes "why is it a const fn?". There are two reasons. First, const functions can be used not only to directly initialize a const item, but also in const expressions and other const functions. It is desirable to have the const functions be as close as possible to ordinary Rust code in their capabilities, so that code and developer skills can be reused. But there is nothing wrong in creating a RefCell inside of a const block/const fn and using is just for temporary storage during evaluation! The case of assigning RefCell::new to a global const item becomes just a consequence of general rules. A bit of a degenerate case and likely undesirable, but that is a lint-level consideration, not an implementation issue warranting a compiler error.
The second reason is that it is very useful to create safe "immutable" static items with interior mutability:
This pattern is even more useful in multithreaded code, with a global state guarded by a Mutex. But the rules for expressions which can initialize statics are mostly the same as for initializers of consts, because a static must be similarly fully computable at compile time.
Caveat: Freeze does not indicate that a type exposes no shared mutability. It only indicates that a type does not have any UnsafeCell before indirection. Box<Mutex<_>> is Freeze.
thread_local! with const initializer tends to generate much more efficient code since it doesn't have to do lazy initialization. With proper tls-model it can even be as efficient as unsynchronized mutable statics.
Having Mutex::new and Vec::new and HashMap::new being const is extremely helpful for initializing static variables.
And I have to stress that, It's not const variable, it's const item. You are not declaring a variable with const statement.