As I understand, mutability is not reflected in variables type signature. For example, these two references have the same type signature &i32
:
let ref_foo : &i32 = &foo;
let mut ref_bar : &i32 = &bar;
Why is this the case? It seems like a pretty major oversight. I mean, even C/C++ does this more explictly with having two const
to indicate that we have a const
pointer to const
data:
const int * const ptr_foo = &foo;
const int * ptr_bar = &bar;
Is there a better way of thinking about this?
However, in Rust, mutability is an all-or-nothing attribute: either a variable is declared as mutable and all of its fields are also mutable (if it is a struct ), or it’s declared immutable and so are all of its fields. How do we get selective field mutability?
When you move a value into a new variable (via a let or a function call), you can change the mutability of the variable The mutability and names of variables in function signatures do not affect the signature of the function The mutability of a reference is built into the type itself, so you can't "upgrade" an immutable reference to a mutable one
("The value of x is {}", x); x = 6; println! ("The value of x is {}", x); } This ambiguity in how variables are (not really) protected from reassignment seems contrary to the stated goal of protecting the values bound to immutable - by Rust default - variables.
Fortunately for us, the Rust standard library provides two wrappers, std::cell::Cell and std::cell::RefCell, that allow us to introduce interior mutability in externally immutable instances of data structures. With Cell<T> and RefCell<T> in our collective toolbelts, we can harness the power of interior mutability.
Mutability is a property of a binding in Rust, not a property of the type.
The sole owner of a value can always mutate it by moving it to a mutable binding:
let s = "Hi".to_owned(); // Create an owned value.
s.push('!'); // Error because s is immutable.
let mut t = s; // Move owned value to mutable binding.
t.push('!'); // Now we can modify the string.
This shows that mutability is not a property of the type of a value, but rather of its binding. The code of course only works if the value isn't currently borrowed, which would block moving the value. A shared borrow is still guaranteed to be immutable.
Mutability of references is orthogonal to mutability of bindings. Rust uses the same mut
keyword to disambiguate the two types of references, but it's a separate concept.
The interior mutability pattern is again orthogonal to the above, as it is part of the type. Types containing a Cell
, RefCell
or similar can be modified even when only holding a shared reference to them.
It's a common pattern to rebind a value as immutable once you are done mutating a value:
let mut x = ...;
// modify x ...
let x = x;
Ownership semantics and the type system in Rust are somewhat different than C++, and I prefer the Rust way. I don't think it's inherently less expressive, as you seem to suggest.
Constants in C++ and Rust are fundamentally different. In C++ constness is a property of any type, while in Rust it is a property of a reference. Thus, in Rust there are not true constant types.
Take for example this C++ code:
void test() {
const std::string x;
const std::string *p = &x;
const std::string &r = x;
}
Variable x
is declared of constant type, so any reference created to it will be also to constant, and any attempt to modify it (with const_cast
for exampe) will render undefined behavior. Note how const
is part of the type of the object.
In Rust, however, there is no way to declare a constant variable:
fn test() {
let x = String::new();
let r = &x;
let mut x = x; //moved, not copied, now it is mutable!
let r = &mut x;
}
Here, the const-ness or mut-ness is not part of the type of the variable, but a property of each reference. And even the original name of the variable can be considered a reference.
Because when you declare a local variable, either in C++ or Rust, you are actually doing two things:
When you write a C++ constant you are making both constant, the object and the reference. But in Rust there are no constant objects, so only the reference is constant. If you move the object you dispose the original name and bind to a new one, that may or may not be mutable.
Note that in C++ you cannot move a constant object, it will remain constant forever.
About having two consts
for pointers, they are just the same in Rust, if you have two indirections:
fn test() {
let mut x = String::new();
let p: &mut String = &mut x;
let p2: &&mut String = &p;
}
About what is better, that is a matter of taste, but remember all the weird things that a constant can do in C++:
mutable
is not part of the type system, while Rust's Cell/RefCell
are.If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With