Rc and Arc for dummies

It would be nice if someone could explain them to me in depth.

The example in the book just flies over it and I don't think I really got it.

When I look and other examples of reference counting, I always find some with garbage collection.

The only thing I got right now is that they are needed if I have to share a resource to somehow trick the ownership model.

1 Like

In this video (starting at 17:20), Alex Crichton explains the memory management via reference counting (the standard "Rc" and its atomic variant "Arc"):

Please note that this video is based on an older version of Rust.

Regards,

7 Likes

Is there anything in particular you have questions about? For example: when to use reference counting? How you might implement a reference-counting system? How to use Rc specifically? This might help us answer your questions more effectively!

I do tend to think of Reference Counting as a type of garbage collection, even in the context of rust (but you have to look at the term in a slightly more generic context). You can think of "garbage collection" as meaning "how do I know if I can free this resource (like memory)", then reference counting is a way to do this.

As an analogy, let say you are sitting in your living room watching TV with your family, and you want to know when you can turn the TV. Here is what most people do in this scenario: when they leave the room, they look to see if there is still someone in the room watching TV. If there is, they just leave the room and leave the TV on. But when the last person leaves the room, they see that no one else is watching TV, and turn it off as the leave. Let's call this "TV watcher counting" :smile:

To make this analogy slightly more accurate, we have to figure out what we mean by a "reference". Let's say that not everyone in the room is watching TV -- some people are reading books. When someone sits down to watch TV, they put a little sticky note on the TV, which says "I am watching this". When they leave the room, they remove their little sticky note. Instead of looking to see if anyone is in the room before turning off the TV, they look to see if there are any sticky notes left. If not, turn off the TV! In this example, the sticky notes are references.

8 Likes

Ah.

The question was more about when and how to use Rc and Arc.

I kinda got it with threads, but I don't get it without them, since only one function at a time can run and so every resource can only be owned by this function.

It's not quite true that only one function can run at a time, but that's not relevant to how rust tracks "ownership". To quote the very excellent rust book:

Variable bindings have a property in Rust: they ‘have ownership’ of what they’re bound to. This means that when a binding goes out of scope, the resource that they’re bound to are freed.

If we look at the example from the docs

struct Owner {
    name: String
}

struct Gadget {
    id: i32,
    owner: Rc<Owner>
}

we can start to see the problem that Rc is trying to solve here. Since a variable (like Gadget.owner) owns the thing it is bound to, we can't have multiple variables owning the same thing. This would be a problem if we had multiple Gadgets with the same Owner struct. Instead of having Gadget.owner be an Owner directly, it can be an Rc<Owner>. The Rc object is the thing that actually owns the Owner. Conceptually there is only one Rc object, but in reality whenever we clone it we are creating multiple Rc objects that point to the same underlying data.

(I hope that made sense, it was a little confusing with two meanings of "owner")

You now might be asking yourself: How come multiple Rc objects can each own the same data, but I can't? The answer is that Rc uses unsafe code to play some tricks. Here "unsafe" means the compiler can't verify the safety of the code, so the programmer has to say "trust me, this is OK".

4 Likes

Yes, this example is a bit confusing.

The Gadgets own their Owner?

Why doesn't the Owner simply have a Vector<Gadget>?

It feels a bit modeled against the language and then fixed with Rc in this example.

Edit:

After reading it a second time, I think I got it.

So Arc and Rc are the same, just that Arc is slower because it works atomic, but it can be used in threads.

Can they both be used transparently (besides clone())?

This is the answer I get when I am asking about similar problem. hope it help.

2 Likes

Yep! And since both impl the Deref trait, I think it's accurate to say that you can use them transparently (no edge cases come to mind at the moment)

1 Like
let five = Rc::new(5);
let seven = Rc::new(7);

println!("{} + {} = {}", five, seven, five + seven);

Didn't work.

print!("{} + {} = {}", five, seven, *five + *seven);

Did work. Doesn't seem to be transparent :\

Is this some special case for primtives again?

Hmm. More likely I'm just wrong when I said "can use them transparently". I thought maybe rust would automatically dereference those, but I guess not!

1 Like

Because of Add trait isn't implemented for Rc<_>, and the compiler knows that that's not a case to transparently deref it, you get an error about Binary operation cannot be applied to this type. So, you need to explicitly deref the reference counter and then you can do such operation.

4 Likes

Not every trait in existence is implemented for Rc/Arc of a type. For things which are implemented transparently, see Rc in std::rc - Rust. Ord, Hash, Display, Debug, Pointer and Borrow are implemented - I guess Add/Sub/Mul just weren't general enough to get inclusion in this. It is fairly easy to just dereference though.

2 Likes

If I understand correctly, this also means that when you call .clone() on a Rc<T> or Arc<T> object you're just cloning a pointer and not the underlying data, which means that for larger data structures this is also more performant and efficient, correct? I'm thinking of the use-cases when you're working with threads/closures and sometimes have to clone some values captured by the closure in order to avoid move errors, etc.

4 Likes