Right now im back on my track to get better understanding on rust and im kinda bothered by rc.
Do I lack intelligence to envision what can go wrong or there is something wrong with the design of that one. Rc is meant to be used in single threaded context and the borrowing contract shouldn't applied to it because its not possible for rc to be expired aka not reference but value that behave like one. If that is true, why it has run time cost for other writable references and refcell even can panic. Im pretty sure I can make such a mistake in single/current scope there should be other tools to avoid that other then runtime cost. Please, someone with insight, help me to understand that
I read your post twice, but still I have some difficulty to see your exact point. May I ask if you read Smart pointers - Rust for C-Programmers? Well I guess not, only very few have, but I would like to get a few concrete questions to that chapter, so that I can add a few more details (I kept it very short by intent.)
The runtime overhead exists because reference counting and borrow tracking are both runtime effects. If you can statically verify that your loans never outlive the owner and that you do not have mutable aliasing, you would not have a need for Rc
or RefCell
.
Also, Rc
and RefCell
are unrelated but often used together. Specifically, the Rc<RefCell<T>>
composition is very useful because it provides both sharing and mutability. And RefCell<Rc<T>>
is less interesting because it only provides replaceable immutable pointers that can be copied.
Thanks, I just read it and it just state how things are, not why they are like that
I understand that my speech pattern could become strange at times.
I will ask my question in simplified, what could go wrong if rc was providing mutability on itself.
Consider me as stupid c++ developer and if not too bothering could you provide small example or link with such
That is an interesting question. I had a similar understanding problem when I started with Rust -- why in single threaded code only one mutable reference is allowed in Rust. I think the C example at the bottom of Borrowing rules in detail - Rust for C-Programmers is a possible explanation. But I agree, it is sometimes hard to see the advantages of all the Rust restrictions.
Well, C++ developers generally do know about things like iterator invalidation, right? You can't start iterating over some collection and simultaneously modify it - you might run into some invalid states, like pointers pointing to already-freed data without you noticing. That's an example of single-threaded shared mutability and what can go wrong with it.
For more professional discussion, I might recommend this article as reference.
This is unlikely to be the answer you are looking for, but consider:
pub type RcCell<T> = Rc<RefCell<T>>;
This is strictly less useful than separate types which can be composed in other ways with other types. But it has the property you are asking about.
This is how I think of it.
Access to normal Rust variables and references -- &
and &mut
-- is checked by the compiler without any runtime cost. However, this checking is based on scopes in the language {...}
, which can be nested but must be in a simple tree structure. Function calls extend that tree of scopes to the called function, when references are passed as parameters.
A variable in a parent scope can be accessed safely by a child scope using references, due to Rust's borrow checker. But all sharing of data using references must conform to these tree structures.
When you need sharing that does not conform to this tree of scopes, the compiler can't prove that sharing is safe using simple references alone. In those cases, runtime checks are necessary instead to ensure safety. That's when you need Rc/Arc
for sharing. If you also need mutability of the shared data, then you need Rc<RefCell>/Arc<Mutex>
.
In addition to the correctness concerns listed above, there’s also the issue that implicit shared mutability inhibits some desirable optimizations: With Rust’s guarantees, the compiler can reorder and combine reads and writes more freely, without having to worry about the side effects of some function call changing the result of those operations.
Single-threadedness does not prevent mutable access conflicts — iterator invalidation as already mentioned is an easy one. Let's demonstrate that in practice. Rc
actually does provide mutability (Rc::get_mut
) under the condition that the reference count is currently 1, but we can also ask it to bypass that:
#![feature(get_mut_unchecked)]
use std::rc::Rc;
fn main() {
let mut rc1: Rc<Vec<i32>> = Rc::new(vec![1, 2, 3]);
let mut rc2 = rc1.clone();
// SAFETY: Nope.
let mutable_access_1 = unsafe { Rc::get_mut_unchecked(&mut rc1) };
let mutable_access_2 = unsafe { Rc::get_mut_unchecked(&mut rc2) };
for _ in 1..100 {
for elem in mutable_access_1.iter_mut() {
mutable_access_2.push(*elem);
}
}
}
This program will segfault because the Vec::push()
reallocates the same Vec
that is being iterated over. Yet the borrow checker is happy, because rc1
and rc2
are separate owned values that are separately borrowed mutably.
(It's also possible to have something structurally equivalent to multi-threaded access by using async
code which can interleave two subtasks.)
The rule of Rust is that you can have sharing OR mutability, but not both at once. Rc::get_mut()
ensures this by giving you mutability only if there is currently no sharing. Rc<RefCell<_>>
ensures this by giving you mutability only if there is currently no other access to the cell. Borrow checking ensures this by proving the code won’t ever try to do both at once. But if Rc
offered unconditional mutability, that just wouldn’t be sound.
Could you explain this sentence? It's not clear. That's one reason you're getting a variety of replies on different aspects of the topic.
If you mean it's not possible for it to return a dangling pointer -- that's true for references to the immediate value contained within, but orthogonal to being single-threaded. An Arc<_>
can't do that either. A shared reference &_
can't do that either! Borrow checking is about more than immediately preventing dangling pointers and more than just preventing dangling pointers period (even though the Book puts so much emphasis on that).
Borrow checking also enforces
&mut _
being exclusive&_
in the absence ofUnsafeCell
being immutable
And violations of those are UB (and that also does not change in a single threaded context).
UB is, in a sense, as far as one needs to think about it. But if you want to keep asking why, @kpreid's example demonstrates a segfault even though the reference to the immediate value (the Vec
) did not dangle. The properties above are relied upon for correctness: Vec::push
is free to reallocate and move the elements within because the &mut self
it takes ensures exclusivity (provided UB has not occured).
The properties can also be relied upon for upholding logical invariants, avoiding data races, performing compiler optimizations, and so on.
Ok, thanks for your link that's another issue that I have good grasp on it. The problem with the iterator and all of the examples in that articals are about dangling pointers to not owned data. the rc or shared pointer own the data in some sense. The only issue for this kind with rc/shared pointer is if I have multiple instances pointing to the same data and I decide to reinitialize one of them with different value, then the rest wont sync
your example might open my eyes to the problem, I think the problem lies in the deref trait. It probably is applied recursively and with that Rc::get_mut_unchecked(&mut rc1) you actually get pointer to the internal "slice" and that might can become dangling and rust do not have the tools to check for the references produced for constituents of the owned data
Is any one else agree on that, is that the solution ?
The shared Rc is a pointer to the value. They all have the same pointer. If one owner changes the value, all owners point to the changed data.
https://doc.rust-lang.org/std/rc/index.html
The type
Rc<T>
provides shared ownership of a value of typeT
, allocated in the heap. Invokingclone
onRc
produces a new pointer to the same allocation in the heap. When the lastRc
pointer to a given allocation is destroyed, the value stored in that allocation (often referred to as “inner value”) is also dropped.
I guess, my english is bad :(. Doesnt matter, I think I get it. Not sure though
Rust only allows one exclusive &mut
reference or multiple shared &
references to the data or to parts of the data. So there is nothing missing in Rust.
There are no languages I know of that can avoid any runtime cost and guarantee safety when sharing data via pointers, in all cases. Rust does avoid this runtime cost in the limited scenarios where references can be used in the same scope or a nested scope. In such scenarios, you don't need Rc (or Arc).
If you have a scenario where you think there should be no runtime cost, but you've had to use Rc, please post it.
Languages that use tracing garbage collection can provide safety when sharing "references" (this term has a different meaning in other languages), but this garbage collection has a runtime cost of course.
This has nothing to do with the deref trait, but yeah, the issue is with dereferencing. In fact Cell
pretty much provides shared mutability but without the ability to dereference pointers inside it, unless they can be copied out of it.
I can highly recommend this video by @jonhoo where he implements RC from scratch. It's really insightful and helped me understand the concepts. https://www.youtube.com/watch?v=8O0Nt9qY_vo